Browse Source

neon: Implement Material 3 theme

pull/94/head
jld3103 2 years ago
parent
commit
9e911c48a1
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 33
      packages/neon/integration_test/screenshot_test.dart
  2. 1
      packages/neon/lib/l10n/en.arb
  3. 6
      packages/neon/lib/l10n/localizations.dart
  4. 3
      packages/neon/lib/l10n/localizations_en.dart
  5. 51
      packages/neon/lib/src/app.dart
  6. 210
      packages/neon/lib/src/apps/notes/pages/note.dart
  7. 1
      packages/neon/lib/src/apps/notifications/pages/main.dart
  8. 6
      packages/neon/lib/src/pages/home/home.dart
  9. 3
      packages/neon/lib/src/pages/settings/settings.dart
  10. 2
      packages/neon/lib/src/utils/confirmation_dialog.dart
  11. 8
      packages/neon/lib/src/utils/global_options.dart
  12. 113
      packages/neon/lib/src/utils/theme.dart

33
packages/neon/integration_test/screenshot_test.dart

@ -15,6 +15,7 @@ import 'package:neon/src/neon.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';
import 'package:settings/settings.dart';
import 'package:shared_preferences/shared_preferences.dart';
class MemorySharedPreferences implements SharedPreferences {
@ -169,21 +170,25 @@ Future pumpAppPage(
],
child: StreamBuilder<NextcloudTheme>(
stream: userThemeStream,
builder: (final context, final themeSnapshot) => StreamBuilder<ThemeMode>(
stream: globalOptions.themeMode.stream,
builder: (final context, final themeModeSnapshot) => StreamBuilder<bool>(
stream: globalOptions.themeOLEDAsDark.stream,
builder: (final context, final themeOLEDAsDarkSnapshot) => MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: getThemeFromNextcloudTheme(
themeSnapshot.data,
themeModeSnapshot.data ?? ThemeMode.system,
Brightness.light,
oledAsDark: themeOLEDAsDarkSnapshot.data ?? false,
builder: (final context, final themeSnapshot) => OptionBuilder(
option: globalOptions.themeMode,
builder: (final context, final themeMode) => OptionBuilder(
option: globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDark) => OptionBuilder(
option: globalOptions.themeKeepOriginalAccentColor,
builder: (final context, final themeKeepOriginalAccentColor) => MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: getThemeFromNextcloudTheme(
themeSnapshot.data,
themeMode ?? ThemeMode.system,
Brightness.light,
oledAsDark: themeOLEDAsDark ?? false,
keepOriginalAccentColor: themeKeepOriginalAccentColor ?? true,
),
home: builder(context, userThemeStream.add),
),
home: builder(context, userThemeStream.add),
),
),
),

1
packages/neon/lib/l10n/en.arb

@ -80,6 +80,7 @@
"globalOptionsThemeModeDark": "Dark",
"globalOptionsThemeModeAutomatic": "Automatic",
"globalOptionsThemeOLEDAsDark": "OLED theme as dark theme",
"globalOptionsThemeKeepOriginalAccentColor": "Keep the original accent color",
"globalOptionsPushNotificationsNotice": "External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.",
"globalOptionsPushNotificationsEnabled": "Enabled",
"globalOptionsPushNotificationsEnabledDisabledNotice": "No UnifiedPush distributor could be found. Please go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications",

6
packages/neon/lib/l10n/localizations.dart

@ -401,6 +401,12 @@ abstract class AppLocalizations {
/// **'OLED theme as dark theme'**
String get globalOptionsThemeOLEDAsDark;
/// No description provided for @globalOptionsThemeKeepOriginalAccentColor.
///
/// In en, this message translates to:
/// **'Keep the original accent color'**
String get globalOptionsThemeKeepOriginalAccentColor;
/// No description provided for @globalOptionsPushNotificationsNotice.
///
/// In en, this message translates to:

3
packages/neon/lib/l10n/localizations_en.dart

@ -171,6 +171,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get globalOptionsThemeOLEDAsDark => 'OLED theme as dark theme';
@override
String get globalOptionsThemeKeepOriginalAccentColor => 'Keep the original accent color';
@override
String get globalOptionsPushNotificationsNotice =>
'External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.';

51
packages/neon/lib/src/app.dart

@ -92,29 +92,34 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
@override
Widget build(final BuildContext context) => StreamBuilder<Brightness>(
stream: _platformBrightness,
builder: (final context, final platformBrightnessSnapshot) => StreamBuilder<ThemeMode>(
stream: widget.globalOptions.themeMode.stream,
builder: (final context, final themeModeSnapshot) => StreamBuilder<bool>(
stream: widget.globalOptions.themeOLEDAsDark.stream,
builder: (final context, final themeOLEDAsDarkSnapshot) {
if (!platformBrightnessSnapshot.hasData ||
!themeOLEDAsDarkSnapshot.hasData ||
!themeModeSnapshot.hasData) {
return Container();
}
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: _navigatorKey,
theme: getThemeFromNextcloudTheme(
_userTheme,
themeModeSnapshot.data!,
platformBrightnessSnapshot.data!,
oledAsDark: themeOLEDAsDarkSnapshot.data!,
),
home: Container(),
);
},
builder: (final context, final platformBrightnessSnapshot) => OptionBuilder(
option: widget.globalOptions.themeMode,
builder: (final context, final themeMode) => OptionBuilder(
option: widget.globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDark) => OptionBuilder(
option: widget.globalOptions.themeKeepOriginalAccentColor,
builder: (final context, final themeKeepOriginalAccentColor) {
if (!platformBrightnessSnapshot.hasData ||
themeMode == null ||
themeOLEDAsDark == null ||
themeKeepOriginalAccentColor == null) {
return Container();
}
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: _navigatorKey,
theme: getThemeFromNextcloudTheme(
_userTheme,
themeMode,
platformBrightnessSnapshot.data!,
oledAsDark: themeOLEDAsDark,
keepOriginalAccentColor: themeKeepOriginalAccentColor,
),
home: Container(),
);
},
),
),
),
);

210
packages/neon/lib/src/apps/notes/pages/note.dart

@ -85,120 +85,112 @@ class _NotesNotePageState extends State<NotesNotePage> {
}
@override
Widget build(final BuildContext context) {
final titleInputBorder = UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.onPrimary,
),
);
return WillPopScope(
onWillPop: () async {
_update();
if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) {
await Wakelock.disable();
}
return true;
},
child: Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: TextField(
controller: _titleController,
focusNode: _titleFocusNode,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onPrimary,
),
cursorColor: Theme.of(context).colorScheme.onPrimary,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: titleInputBorder,
focusedBorder: titleInputBorder,
),
),
actions: [
IconButton(
icon: Icon(
_synced ? Icons.check : Icons.sync,
Widget build(final BuildContext context) => WillPopScope(
onWillPop: () async {
_update();
if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) {
await Wakelock.disable();
}
return true;
},
child: Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
titleSpacing: 0,
title: TextField(
controller: _titleController,
focusNode: _titleFocusNode,
style: const TextStyle(
fontSize: 22,
),
onPressed: _update,
),
IconButton(
icon: Icon(
_showEditor ? Icons.visibility : Icons.edit,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: UnderlineInputBorder(),
focusedBorder: UnderlineInputBorder(),
),
onPressed: () {
setState(() {
_showEditor = !_showEditor;
});
if (_showEditor) {
_focusEditor();
} else {
// Prevent the cursor going back to the title field
_contentFocusNode.unfocus();
_titleFocusNode.unfocus();
}
},
),
IconButton(
onPressed: () async {
final result = await showDialog<String>(
context: context,
builder: (final context) => NotesSelectCategoryDialog(
bloc: widget.bloc,
note: _note,
),
);
if (result != null) {
_update(result);
}
},
icon: Icon(
MdiIcons.tag,
color: _note.category.isNotEmpty ? NotesCategoryColor.compute(_note.category) : null,
actions: [
IconButton(
icon: Icon(
_synced ? Icons.check : Icons.sync,
),
onPressed: _update,
),
),
],
),
body: GestureDetector(
onTap: () {
setState(() {
_showEditor = true;
});
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 10,
horizontal: _showEditor ? 20 : 10,
),
color: Colors.transparent,
constraints: const BoxConstraints.expand(),
child: _showEditor
? TextField(
controller: _contentController,
focusNode: _contentFocusNode,
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
IconButton(
icon: Icon(
_showEditor ? Icons.visibility : Icons.edit,
),
onPressed: () {
setState(() {
_showEditor = !_showEditor;
});
if (_showEditor) {
_focusEditor();
} else {
// Prevent the cursor going back to the title field
_contentFocusNode.unfocus();
_titleFocusNode.unfocus();
}
},
),
IconButton(
onPressed: () async {
final result = await showDialog<String>(
context: context,
builder: (final context) => NotesSelectCategoryDialog(
bloc: widget.bloc,
note: _note,
),
);
if (result != null) {
_update(result);
}
},
icon: Icon(
MdiIcons.tag,
color: _note.category.isNotEmpty ? NotesCategoryColor.compute(_note.category) : null,
),
),
],
),
body: GestureDetector(
onTap: () {
setState(() {
_showEditor = true;
});
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 10,
horizontal: _showEditor ? 20 : 10,
),
color: Colors.transparent,
constraints: const BoxConstraints.expand(),
child: _showEditor
? TextField(
controller: _contentController,
focusNode: _contentFocusNode,
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
),
)
: MarkdownBody(
data: _contentController.text,
onTapLink: (final text, final href, final title) async {
if (href != null) {
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}
},
),
)
: MarkdownBody(
data: _contentController.text,
onTapLink: (final text, final href, final title) async {
if (href != null) {
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}
},
),
),
),
),
),
);
}
);
}

1
packages/neon/lib/src/apps/notifications/pages/main.dart

@ -112,6 +112,7 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: () {
Navigator.of(context).pop();

6
packages/neon/lib/src/pages/home/home.dart

@ -280,6 +280,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: () {
Navigator.of(context).pop();
@ -427,7 +428,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
}
return DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).appBarTheme.backgroundColor,
color: Theme.of(context).colorScheme.primary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -461,7 +462,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor,
iconEnabledColor: Theme.of(context).colorScheme.onBackground,
value: widget.account.id,
items: accounts
.map<DropdownMenuItem<String>>(
@ -607,6 +608,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
);
return Scaffold(
resizeToAvoidBottomInset: false,
body: Row(
children: [
if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[

3
packages/neon/lib/src/pages/settings/settings.dart

@ -105,6 +105,9 @@ class _SettingsPageState extends State<SettingsPage> {
CheckBoxSettingsTile(
option: globalOptions.themeOLEDAsDark,
),
CheckBoxSettingsTile(
option: globalOptions.themeKeepOriginalAccentColor,
),
],
),
SettingsCategory(

2
packages/neon/lib/src/utils/confirmation_dialog.dart

@ -10,6 +10,7 @@ Future<bool> showConfirmationDialog(final BuildContext context, final String tit
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: () {
Navigator.of(context).pop(false);
@ -19,6 +20,7 @@ Future<bool> showConfirmationDialog(final BuildContext context, final String tit
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: () {
Navigator.of(context).pop(true);

8
packages/neon/lib/src/utils/global_options.dart

@ -60,6 +60,7 @@ class GlobalOptions {
late final List<Option> options = [
themeMode,
themeOLEDAsDark,
themeKeepOriginalAccentColor,
pushNotificationsEnabled,
pushNotificationsDistributor,
startupMinimized,
@ -135,6 +136,13 @@ class GlobalOptions {
enabled: _themeOLEDAsDarkEnabledSubject,
);
late final themeKeepOriginalAccentColor = ToggleOption(
storage: _storage,
key: 'theme-keep-original-accent-color',
label: (final context) => AppLocalizations.of(context).globalOptionsThemeKeepOriginalAccentColor,
defaultValue: BehaviorSubject.seeded(false),
);
late final pushNotificationsEnabled = ToggleOption(
storage: _storage,
key: 'push-notifications-enabled',

113
packages/neon/lib/src/utils/theme.dart

@ -1,20 +1,15 @@
part of '../neon.dart';
const themePrimaryColor = Color(0xFFF37736);
const themeOnPrimaryColor = Color(0xFFFFFFFF);
ThemeData getThemeFromNextcloudTheme(
final CoreServerCapabilities_Ocs_Data_Capabilities_Theming? nextcloudTheme,
final ThemeMode themeMode,
final Brightness platformBrightness, {
required final bool oledAsDark,
required final bool keepOriginalAccentColor,
}) {
var primaryColor = themePrimaryColor;
var onPrimaryColor = themeOnPrimaryColor;
if (nextcloudTheme != null) {
primaryColor = HexColor(nextcloudTheme.color);
onPrimaryColor = HexColor(nextcloudTheme.colorText);
}
final primaryColor = nextcloudTheme != null ? HexColor(nextcloudTheme.color) : themePrimaryColor;
late final Brightness selectBrightness;
switch (themeMode) {
@ -29,62 +24,24 @@ ThemeData getThemeFromNextcloudTheme(
break;
}
final backgroundColor = selectBrightness == Brightness.dark
? oledAsDark
? Colors.black
: const Color(0xFF303030)
: Colors.white;
final onBackgroundColor = selectBrightness == Brightness.dark ? Colors.white : const Color(0xFF303030);
final canvasColor = selectBrightness == Brightness.dark
? oledAsDark
? const Color(0xFF202020)
: const Color(0xFF404040)
: const Color(0xFFEAEAEA);
final disabledColor = selectBrightness == Brightness.dark
? oledAsDark
? Colors.grey[700]
: Colors.grey[600]
: Colors.grey[500];
final colorScheme =
(selectBrightness == Brightness.dark ? const ColorScheme.dark() : const ColorScheme.light()).copyWith(
primary: primaryColor,
onPrimary: onPrimaryColor,
secondary: primaryColor,
onSecondary: onPrimaryColor,
background: backgroundColor,
onBackground: onBackgroundColor,
final oledBackgroundOverride = selectBrightness == Brightness.dark && oledAsDark ? Colors.black : null;
final colorScheme = ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: selectBrightness,
).copyWith(
background: oledBackgroundOverride,
surface: oledBackgroundOverride,
primary: keepOriginalAccentColor ? primaryColor : null,
);
return ThemeData(
useMaterial3: true,
disabledColor: disabledColor,
brightness: selectBrightness,
scaffoldBackgroundColor: backgroundColor,
canvasColor: canvasColor,
cardColor: backgroundColor,
colorScheme: colorScheme,
textSelectionTheme: TextSelectionThemeData(
cursorColor: primaryColor,
selectionColor: primaryColor,
selectionHandleColor: primaryColor,
),
appBarTheme: AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: selectBrightness == Brightness.dark ? Colors.white : Colors.black,
),
snackBarTheme: SnackBarThemeData(
scaffoldBackgroundColor: colorScheme.background,
canvasColor: colorScheme.background, // For Drawer
cardColor: colorScheme.background, // For LicensePage
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
actionTextColor: primaryColor,
),
progressIndicatorTheme: ProgressIndicatorThemeData(
color: primaryColor,
),
drawerTheme: DrawerThemeData(
backgroundColor: backgroundColor,
),
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.resolveWith((final states) {
@ -92,43 +49,21 @@ ThemeData getThemeFromNextcloudTheme(
return selectBrightness == Brightness.dark ? Colors.white38 : Colors.black38;
}
return primaryColor;
return colorScheme.primary;
}),
checkColor: MaterialStateProperty.resolveWith((final states) => onPrimaryColor),
checkColor: MaterialStateProperty.resolveWith((final states) => colorScheme.onPrimary),
),
dividerTheme: DividerThemeData(
color: primaryColor,
dividerTheme: const DividerThemeData(
thickness: 1.5,
space: 40,
indent: 10,
endIndent: 10,
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.resolveWith(
(final states) => primaryColor
.withOpacity(states.contains(MaterialState.hovered) || states.contains(MaterialState.dragged) ? 1 : 0.5),
),
mainAxisMargin: 10,
crossAxisMargin: 5,
),
radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith(
(final states) => states.contains(MaterialState.disabled) ? disabledColor : primaryColor,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: onPrimaryColor,
backgroundColor: primaryColor,
).copyWith(
elevation: ButtonStyleButton.allOrNull(0),
),
space: 30,
),
popupMenuTheme: PopupMenuThemeData(
color: canvasColor,
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: primaryColor,
// TODO: Only needed until M3 popup menus are implemented
color: selectBrightness == Brightness.dark
? oledAsDark
? const Color(0xFF202020)
: const Color(0xFF404040)
: const Color(0xFFEAEAEA),
),
);
}

Loading…
Cancel
Save