From ff83c2cc63c417853ef56a3bdc9d0e3409771154 Mon Sep 17 00:00:00 2001 From: "Dr.Blank" Date: Sat, 5 Oct 2024 10:01:08 -0400 Subject: [PATCH] feat: multiple theming options (#50) * refactor: consolidate theme definitions by removing separate dark and light theme files * feat: integrate dynamic color support and enhance theme settings management * feat: add theme settings route and update theme management in app settings * feat: enhance theme management by integrating high contrast support in various components * feat: implement mode selection dialog for theme settings and enhance button functionality * refactor: update theme import paths and consolidate theme provider files * feat: enhance theme management by integrating theme selection based on audiobook playback * refactor: update default value for useMaterialThemeFromSystem to false in theme settings * refactor: adjust high contrast condition order in theme settings for consistency * refactor: rename useMaterialThemeOfPlayingItem to useCurrentPlayerThemeThroughoutApp for clarity * refactor: correct spelling in system theme provider and replace with updated implementation * refactor: extract restore backup dialog into a separate widget for improved readability * refactor: reorganize settings sections for clarity and improve restore dialog functionality --- .vscode/settings.json | 1 + .../view/library_item_hero_section.dart | 11 +- .../player/view/audiobook_player.dart | 4 +- lib/main.dart | 77 +++++- lib/router/constants.dart | 5 + lib/router/router.dart | 8 + lib/settings/app_settings_provider.dart | 5 - lib/settings/app_settings_provider.g.dart | 2 +- lib/settings/models/app_settings.dart | 5 +- lib/settings/models/app_settings.freezed.dart | 114 ++++++-- lib/settings/models/app_settings.g.dart | 18 +- lib/settings/view/app_settings_page.dart | 247 +++++++++-------- lib/settings/view/buttons.dart | 4 + lib/settings/view/theme_settings_page.dart | 261 ++++++++++++++++++ lib/shared/widgets/shelves/book_shelf.dart | 2 +- lib/theme/dark.dart | 10 - lib/theme/light.dart | 10 - .../providers/system_theme_provider.dart | 73 +++++ .../providers/system_theme_provider.g.dart | 177 ++++++++++++ .../theme_from_cover_provider.dart | 21 +- .../theme_from_cover_provider.g.dart | 40 ++- lib/theme/theme.dart | 16 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 8 + pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 28 files changed, 935 insertions(+), 194 deletions(-) create mode 100644 lib/settings/view/theme_settings_page.dart delete mode 100644 lib/theme/dark.dart delete mode 100644 lib/theme/light.dart create mode 100644 lib/theme/providers/system_theme_provider.dart create mode 100644 lib/theme/providers/system_theme_provider.g.dart rename lib/theme/{ => providers}/theme_from_cover_provider.dart (81%) rename lib/theme/{ => providers}/theme_from_cover_provider.g.dart (86%) diff --git a/.vscode/settings.json b/.vscode/settings.json index ffa9f03..b4c5fa8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "audioplayers", "autolabeler", "Autovalidate", + "Checkmark", "deeplinking", "fullscreen", "Lerp", diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index 83b5555..856c5a5 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -15,7 +15,7 @@ import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; -import 'package:vaani/theme/theme_from_cover_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; class LibraryItemHeroSection extends HookConsumerWidget { const LibraryItemHeroSection({ @@ -353,16 +353,17 @@ class _BookCover extends HookConsumerWidget { final coverImage = ref.watch(coverImageProvider(itemId)); final themeData = Theme.of(context); // final item = ref.watch(libraryItemProvider(itemId)); - final useMaterialThemeOnItemPage = - ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; + final themeSettings = ref.watch(appSettingsProvider).themeSettings; ColorScheme? coverColorScheme; - if (useMaterialThemeOnItemPage) { + if (themeSettings.useMaterialThemeOnItemPage) { coverColorScheme = ref .watch( themeOfLibraryItemProvider( itemId, brightness: Theme.of(context).brightness, + highContrast: themeSettings.highContrast || + MediaQuery.of(context).highContrast, ), ) .valueOrNull; @@ -371,7 +372,7 @@ class _BookCover extends HookConsumerWidget { return ThemeSwitcher( builder: (context) { // change theme after 2 seconds - if (useMaterialThemeOnItemPage) { + if (themeSettings.useMaterialThemeOnItemPage) { Future.delayed(150.ms, () { try { ThemeSwitcher.of(context).changeTheme( diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart index 46ed7aa..1940ba7 100644 --- a/lib/features/player/view/audiobook_player.dart +++ b/lib/features/player/view/audiobook_player.dart @@ -14,7 +14,7 @@ import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/inverse_lerp.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; -import 'package:vaani/theme/theme_from_cover_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'player_when_expanded.dart'; import 'player_when_minimized.dart'; @@ -65,6 +65,8 @@ class AudiobookPlayer extends HookConsumerWidget { themeOfLibraryItemProvider( itemBeingPlayed.valueOrNull?.id, brightness: Theme.of(context).brightness, + highContrast: appSettings.themeSettings.highContrast || + MediaQuery.of(context).highContrast, ), ); diff --git a/lib/main.dart b/lib/main.dart index 7f54ec1..0da35ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; @@ -7,12 +8,15 @@ import 'package:vaani/features/downloads/providers/download_manager.dart'; import 'package:vaani/features/logging/core/logger.dart'; import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart'; import 'package:vaani/features/player/core/init.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart' + show audiobookPlayerProvider, simpleAudiobookPlayerProvider; import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/theme/providers/system_theme_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/theme.dart'; final appLogger = Logger('vaani'); @@ -51,16 +55,77 @@ class MyApp extends ConsumerWidget { if (needOnboarding) { routerConfig.goNamed(Routes.onboarding.name); } + final appSettings = ref.watch(appSettingsProvider); + final themeSettings = appSettings.themeSettings; + ColorScheme lightColorScheme = brandLightColorScheme; + ColorScheme darkColorScheme = brandDarkColorScheme; + + final shouldUseHighContrast = + themeSettings.highContrast || MediaQuery.of(context).highContrast; + + if (shouldUseHighContrast) { + lightColorScheme = lightColorScheme.copyWith( + surface: Colors.white, + ); + darkColorScheme = darkColorScheme.copyWith( + surface: Colors.black, + ); + } + + if (themeSettings.useMaterialThemeFromSystem) { + var themes = + ref.watch(systemThemeProvider(highContrast: shouldUseHighContrast)); + if (themes.valueOrNull != null) { + lightColorScheme = themes.valueOrNull!.$1; + darkColorScheme = themes.valueOrNull!.$2; + } + } + + if (themeSettings.useCurrentPlayerThemeThroughoutApp) { + final player = ref.watch(audiobookPlayerProvider); + if (player.book != null) { + final themeLight = ref.watch( + themeOfLibraryItemProvider( + player.book!.libraryItemId, + highContrast: shouldUseHighContrast, + brightness: Brightness.light, + ), + ); + final themeDark = ref.watch( + themeOfLibraryItemProvider( + player.book!.libraryItemId, + highContrast: shouldUseHighContrast, + brightness: Brightness.dark, + ), + ); + if (themeLight.valueOrNull != null && themeDark.valueOrNull != null) { + lightColorScheme = themeLight.valueOrNull!; + darkColorScheme = themeDark.valueOrNull!; + } + } + } + final appThemeLight = ThemeData( + useMaterial3: true, + colorScheme: lightColorScheme.harmonized(), + ); + final appThemeDark = ThemeData( + useMaterial3: true, + colorScheme: darkColorScheme.harmonized(), + brightness: Brightness.dark, + // TODO bottom sheet theme is not working + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: darkColorScheme.surface, + ), + ); try { return MaterialApp.router( // debugShowCheckedModeBanner: false, - theme: lightTheme, - darkTheme: darkTheme, - themeMode: ref.watch(appSettingsProvider).themeSettings.isDarkMode - ? ThemeMode.dark - : ThemeMode.light, + theme: appThemeLight, + darkTheme: appThemeDark, + themeMode: themeSettings.themeMode, routerConfig: routerConfig, + themeAnimationCurve: Curves.easeInOut, ); } catch (e) { debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); diff --git a/lib/router/constants.dart b/lib/router/constants.dart index 9d01c29..f36d706 100644 --- a/lib/router/constants.dart +++ b/lib/router/constants.dart @@ -27,6 +27,11 @@ class Routes { pathName: 'config', name: 'settings', ); + static const themeSettings = _SimpleRoute( + pathName: 'theme', + name: 'themeSettings', + parentRoute: settings, + ); static const autoSleepTimerSettings = _SimpleRoute( pathName: 'autoSleepTimer', name: 'autoSleepTimerSettings', diff --git a/lib/router/router.dart b/lib/router/router.dart index c5b1a67..5e9f71e 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -17,6 +17,7 @@ import 'package:vaani/settings/view/auto_sleep_timer_settings_page.dart'; import 'package:vaani/settings/view/notification_settings_page.dart'; import 'package:vaani/settings/view/player_settings_page.dart'; import 'package:vaani/settings/view/shake_detector_settings_page.dart'; +import 'package:vaani/settings/view/theme_settings_page.dart'; import 'scaffold_with_nav_bar.dart'; import 'transitions/slide.dart'; @@ -178,6 +179,13 @@ class MyAppRouter { // builder: (context, state) => const AppSettingsPage(), pageBuilder: defaultPageBuilder(const AppSettingsPage()), routes: [ + GoRoute( + path: Routes.themeSettings.pathName, + name: Routes.themeSettings.name, + pageBuilder: defaultPageBuilder( + const ThemeSettingsPage(), + ), + ), GoRoute( path: Routes.autoSleepTimerSettings.pathName, name: Routes.autoSleepTimerSettings.name, diff --git a/lib/settings/app_settings_provider.dart b/lib/settings/app_settings_provider.dart index 6d61774..b86e736 100644 --- a/lib/settings/app_settings_provider.dart +++ b/lib/settings/app_settings_provider.dart @@ -47,11 +47,6 @@ class AppSettings extends _$AppSettings { _logger.fine('wrote settings to box: $state'); } - void toggleDarkMode() { - state = state.copyWith - .themeSettings(isDarkMode: !state.themeSettings.isDarkMode); - } - void update(model.AppSettings newSettings) { state = newSettings; } diff --git a/lib/settings/app_settings_provider.g.dart b/lib/settings/app_settings_provider.g.dart index df95738..85d58cd 100644 --- a/lib/settings/app_settings_provider.g.dart +++ b/lib/settings/app_settings_provider.g.dart @@ -6,7 +6,7 @@ part of 'app_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$appSettingsHash() => r'f51d55f117692d4fb9f4b4febf02906c0953d334'; +String _$appSettingsHash() => r'314d7936f54550f57d308056a99230402342a6d0'; /// See also [AppSettings]. @ProviderFor(AppSettings) diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 80262ea..e486ac4 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -28,7 +28,10 @@ class AppSettings with _$AppSettings { @freezed class ThemeSettings with _$ThemeSettings { const factory ThemeSettings({ - @Default(true) bool isDarkMode, + @Default(ThemeMode.system) ThemeMode themeMode, + @Default(false) bool highContrast, + @Default(false) bool useMaterialThemeFromSystem, + @Default('#FF311B92') String customThemeColor, @Default(true) bool useMaterialThemeOnItemPage, @Default(true) bool useCurrentPlayerThemeThroughoutApp, }) = _ThemeSettings; diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 3f8fd48..fc52047 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -378,7 +378,10 @@ ThemeSettings _$ThemeSettingsFromJson(Map json) { /// @nodoc mixin _$ThemeSettings { - bool get isDarkMode => throw _privateConstructorUsedError; + ThemeMode get themeMode => throw _privateConstructorUsedError; + bool get highContrast => throw _privateConstructorUsedError; + bool get useMaterialThemeFromSystem => throw _privateConstructorUsedError; + String get customThemeColor => throw _privateConstructorUsedError; bool get useMaterialThemeOnItemPage => throw _privateConstructorUsedError; bool get useCurrentPlayerThemeThroughoutApp => throw _privateConstructorUsedError; @@ -400,7 +403,10 @@ abstract class $ThemeSettingsCopyWith<$Res> { _$ThemeSettingsCopyWithImpl<$Res, ThemeSettings>; @useResult $Res call( - {bool isDarkMode, + {ThemeMode themeMode, + bool highContrast, + bool useMaterialThemeFromSystem, + String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp}); } @@ -420,15 +426,30 @@ class _$ThemeSettingsCopyWithImpl<$Res, $Val extends ThemeSettings> @pragma('vm:prefer-inline') @override $Res call({ - Object? isDarkMode = null, + Object? themeMode = null, + Object? highContrast = null, + Object? useMaterialThemeFromSystem = null, + Object? customThemeColor = null, Object? useMaterialThemeOnItemPage = null, Object? useCurrentPlayerThemeThroughoutApp = null, }) { return _then(_value.copyWith( - isDarkMode: null == isDarkMode - ? _value.isDarkMode - : isDarkMode // ignore: cast_nullable_to_non_nullable + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + highContrast: null == highContrast + ? _value.highContrast + : highContrast // ignore: cast_nullable_to_non_nullable as bool, + useMaterialThemeFromSystem: null == useMaterialThemeFromSystem + ? _value.useMaterialThemeFromSystem + : useMaterialThemeFromSystem // ignore: cast_nullable_to_non_nullable + as bool, + customThemeColor: null == customThemeColor + ? _value.customThemeColor + : customThemeColor // ignore: cast_nullable_to_non_nullable + as String, useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage ? _value.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable @@ -451,7 +472,10 @@ abstract class _$$ThemeSettingsImplCopyWith<$Res> @override @useResult $Res call( - {bool isDarkMode, + {ThemeMode themeMode, + bool highContrast, + bool useMaterialThemeFromSystem, + String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp}); } @@ -469,15 +493,30 @@ class __$$ThemeSettingsImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? isDarkMode = null, + Object? themeMode = null, + Object? highContrast = null, + Object? useMaterialThemeFromSystem = null, + Object? customThemeColor = null, Object? useMaterialThemeOnItemPage = null, Object? useCurrentPlayerThemeThroughoutApp = null, }) { return _then(_$ThemeSettingsImpl( - isDarkMode: null == isDarkMode - ? _value.isDarkMode - : isDarkMode // ignore: cast_nullable_to_non_nullable + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + highContrast: null == highContrast + ? _value.highContrast + : highContrast // ignore: cast_nullable_to_non_nullable as bool, + useMaterialThemeFromSystem: null == useMaterialThemeFromSystem + ? _value.useMaterialThemeFromSystem + : useMaterialThemeFromSystem // ignore: cast_nullable_to_non_nullable + as bool, + customThemeColor: null == customThemeColor + ? _value.customThemeColor + : customThemeColor // ignore: cast_nullable_to_non_nullable + as String, useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage ? _value.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable @@ -495,7 +534,10 @@ class __$$ThemeSettingsImplCopyWithImpl<$Res> @JsonSerializable() class _$ThemeSettingsImpl implements _ThemeSettings { const _$ThemeSettingsImpl( - {this.isDarkMode = true, + {this.themeMode = ThemeMode.system, + this.highContrast = false, + this.useMaterialThemeFromSystem = false, + this.customThemeColor = '#FF311B92', this.useMaterialThemeOnItemPage = true, this.useCurrentPlayerThemeThroughoutApp = true}); @@ -504,7 +546,16 @@ class _$ThemeSettingsImpl implements _ThemeSettings { @override @JsonKey() - final bool isDarkMode; + final ThemeMode themeMode; + @override + @JsonKey() + final bool highContrast; + @override + @JsonKey() + final bool useMaterialThemeFromSystem; + @override + @JsonKey() + final String customThemeColor; @override @JsonKey() final bool useMaterialThemeOnItemPage; @@ -514,7 +565,7 @@ class _$ThemeSettingsImpl implements _ThemeSettings { @override String toString() { - return 'ThemeSettings(isDarkMode: $isDarkMode, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp: $useCurrentPlayerThemeThroughoutApp)'; + return 'ThemeSettings(themeMode: $themeMode, highContrast: $highContrast, useMaterialThemeFromSystem: $useMaterialThemeFromSystem, customThemeColor: $customThemeColor, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp: $useCurrentPlayerThemeThroughoutApp)'; } @override @@ -522,8 +573,16 @@ class _$ThemeSettingsImpl implements _ThemeSettings { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ThemeSettingsImpl && - (identical(other.isDarkMode, isDarkMode) || - other.isDarkMode == isDarkMode) && + (identical(other.themeMode, themeMode) || + other.themeMode == themeMode) && + (identical(other.highContrast, highContrast) || + other.highContrast == highContrast) && + (identical(other.useMaterialThemeFromSystem, + useMaterialThemeFromSystem) || + other.useMaterialThemeFromSystem == + useMaterialThemeFromSystem) && + (identical(other.customThemeColor, customThemeColor) || + other.customThemeColor == customThemeColor) && (identical(other.useMaterialThemeOnItemPage, useMaterialThemeOnItemPage) || other.useMaterialThemeOnItemPage == @@ -536,8 +595,14 @@ class _$ThemeSettingsImpl implements _ThemeSettings { @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, isDarkMode, - useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp); + int get hashCode => Object.hash( + runtimeType, + themeMode, + highContrast, + useMaterialThemeFromSystem, + customThemeColor, + useMaterialThemeOnItemPage, + useCurrentPlayerThemeThroughoutApp); /// Create a copy of ThemeSettings /// with the given fields replaced by the non-null parameter values. @@ -557,7 +622,10 @@ class _$ThemeSettingsImpl implements _ThemeSettings { abstract class _ThemeSettings implements ThemeSettings { const factory _ThemeSettings( - {final bool isDarkMode, + {final ThemeMode themeMode, + final bool highContrast, + final bool useMaterialThemeFromSystem, + final String customThemeColor, final bool useMaterialThemeOnItemPage, final bool useCurrentPlayerThemeThroughoutApp}) = _$ThemeSettingsImpl; @@ -565,7 +633,13 @@ abstract class _ThemeSettings implements ThemeSettings { _$ThemeSettingsImpl.fromJson; @override - bool get isDarkMode; + ThemeMode get themeMode; + @override + bool get highContrast; + @override + bool get useMaterialThemeFromSystem; + @override + String get customThemeColor; @override bool get useMaterialThemeOnItemPage; @override diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 90b317e..45bb74b 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -46,7 +46,12 @@ Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) => _$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map json) => _$ThemeSettingsImpl( - isDarkMode: json['isDarkMode'] as bool? ?? true, + themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? + ThemeMode.system, + highContrast: json['highContrast'] as bool? ?? false, + useMaterialThemeFromSystem: + json['useMaterialThemeFromSystem'] as bool? ?? false, + customThemeColor: json['customThemeColor'] as String? ?? '#FF311B92', useMaterialThemeOnItemPage: json['useMaterialThemeOnItemPage'] as bool? ?? true, useCurrentPlayerThemeThroughoutApp: @@ -55,12 +60,21 @@ _$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map json) => Map _$$ThemeSettingsImplToJson(_$ThemeSettingsImpl instance) => { - 'isDarkMode': instance.isDarkMode, + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'highContrast': instance.highContrast, + 'useMaterialThemeFromSystem': instance.useMaterialThemeFromSystem, + 'customThemeColor': instance.customThemeColor, 'useMaterialThemeOnItemPage': instance.useMaterialThemeOnItemPage, 'useCurrentPlayerThemeThroughoutApp': instance.useCurrentPlayerThemeThroughoutApp, }; +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => _$PlayerSettingsImpl( miniPlayerSettings: json['miniPlayerSettings'] == null diff --git a/lib/settings/view/app_settings_page.dart b/lib/settings/view/app_settings_page.dart index 77afaae..aebe867 100644 --- a/lib/settings/view/app_settings_page.dart +++ b/lib/settings/view/app_settings_page.dart @@ -7,11 +7,10 @@ import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; -import 'package:vaani/api/server_provider.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/models/app_settings.dart' as model; +import 'package:vaani/settings/view/buttons.dart'; import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart'; @@ -23,58 +22,11 @@ class AppSettingsPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final appSettings = ref.watch(appSettingsProvider); - final registeredServers = ref.watch(audiobookShelfServerProvider); - final registeredServersAsList = registeredServers.toList(); - final availableUsers = ref.watch(authenticatedUserProvider); - final serverURIController = useTextEditingController(); final sleepTimerSettings = appSettings.sleepTimerSettings; return SimpleSettingsPage( title: const Text('App Settings'), sections: [ - // Appearance section - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - title: Text( - 'Appearance', - style: Theme.of(context).textTheme.titleLarge, - ), - tiles: [ - SettingsTile.switchTile( - initialValue: appSettings.themeSettings.isDarkMode, - title: const Text('Dark Mode'), - description: const Text('we all know dark mode is better'), - leading: appSettings.themeSettings.isDarkMode - ? const Icon(Icons.dark_mode) - : const Icon(Icons.light_mode), - onToggle: (value) { - ref.read(appSettingsProvider.notifier).toggleDarkMode(); - }, - ), - SettingsTile.switchTile( - initialValue: - appSettings.themeSettings.useMaterialThemeOnItemPage, - title: const Text('Adaptive Theme on Item Page'), - description: const Text( - 'get fancy with the colors on the item page at the cost of some performance', - ), - leading: appSettings.themeSettings.useMaterialThemeOnItemPage - ? const Icon(Icons.auto_fix_high) - : const Icon(Icons.auto_fix_off), - onToggle: (value) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.themeSettings( - useMaterialThemeOnItemPage: value, - ), - ); - }, - ), - ], - ), - // General section SettingsSection( margin: const EdgeInsetsDirectional.symmetric( @@ -86,6 +38,16 @@ class AppSettingsPage extends HookConsumerWidget { style: Theme.of(context).textTheme.titleLarge, ), tiles: [ + SettingsTile( + title: const Text('Player Settings'), + leading: const Icon(Icons.play_arrow), + description: const Text( + 'Customize the player settings', + ), + onPressed: (context) { + context.pushNamed(Routes.playerSettings.name); + }, + ), NavigationWithSwitchTile( title: const Text('Auto Turn On Sleep Timer'), description: const Text( @@ -106,26 +68,6 @@ class AppSettingsPage extends HookConsumerWidget { ); }, ), - SettingsTile( - title: const Text('Notification Media Player'), - leading: const Icon(Icons.play_lesson), - description: const Text( - 'Customize the media player in notifications', - ), - onPressed: (context) { - context.pushNamed(Routes.notificationSettings.name); - }, - ), - SettingsTile( - title: const Text('Player Settings'), - leading: const Icon(Icons.play_arrow), - description: const Text( - 'Customize the player settings', - ), - onPressed: (context) { - context.pushNamed(Routes.playerSettings.name); - }, - ), NavigationWithSwitchTile( title: const Text('Shake Detector'), leading: const Icon(Icons.vibration), @@ -146,6 +88,41 @@ class AppSettingsPage extends HookConsumerWidget { ), ], ), + + // Appearance section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + title: Text( + 'Appearance', + style: Theme.of(context).textTheme.titleLarge, + ), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.color_lens), + title: const Text('Theme Settings'), + description: const Text( + 'Customize the app theme', + ), + onPressed: (context) { + context.pushNamed(Routes.themeSettings.name); + }, + ), + SettingsTile( + title: const Text('Notification Media Player'), + leading: const Icon(Icons.play_lesson), + description: const Text( + 'Customize the media player in notifications', + ), + onPressed: (context) { + context.pushNamed(Routes.notificationSettings.name); + }, + ), + ], + ), + // Backup and Restore section SettingsSection( margin: const EdgeInsetsDirectional.symmetric( @@ -185,61 +162,11 @@ class AppSettingsPage extends HookConsumerWidget { 'Restore the app settings from the backup', ), onPressed: (context) { - final formKey = GlobalKey(); // show a dialog to get the backup showDialog( context: context, builder: (context) { - return AlertDialog( - title: const Text('Restore Backup'), - content: Form( - key: formKey, - child: TextFormField( - controller: serverURIController, - decoration: const InputDecoration( - labelText: 'Backup', - hintText: 'Paste the backup here', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please paste the backup here'; - } - return null; - }, - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - if (formKey.currentState!.validate()) { - final backup = serverURIController.text; - final newSettings = model.AppSettings.fromJson( - // decode the backup as json - jsonDecode(backup), - ); - ref - .read(appSettingsProvider.notifier) - .update(newSettings); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings restored'), - ), - ); - // clear the backup - serverURIController.clear(); - } - }, - child: const Text('Restore'), - ), - ], - ); + return RestoreDialogue(); }, ); }, @@ -292,3 +219,83 @@ class AppSettingsPage extends HookConsumerWidget { ); } } + +class RestoreDialogue extends HookConsumerWidget { + const RestoreDialogue({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + final settings = useState(null); + + final settingsInputController = useTextEditingController(); + return AlertDialog( + title: const Text('Restore Backup'), + content: Form( + key: formKey, + child: TextFormField( + autofocus: true, + decoration: InputDecoration( + labelText: 'Backup', + hintText: 'Paste the backup here', + // clear button + suffixIcon: IconButton( + icon: Icon(Icons.clear), + onPressed: () { + settingsInputController.clear(); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please paste the backup here'; + } + try { + // try to decode the backup + settings.value = model.AppSettings.fromJson( + jsonDecode(value), + ); + } catch (e) { + return 'Invalid backup'; + } + return null; + }, + ), + ), + actions: [ + CancelButton(), + TextButton( + onPressed: () { + if (formKey.currentState!.validate()) { + if (settings.value == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invalid backup'), + ), + ); + return; + } + ref.read(appSettingsProvider.notifier).update(settings.value!); + settingsInputController.clear(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Settings restored'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invalid backup'), + ), + ); + } + }, + child: const Text('Restore'), + ), + ], + ); + } +} diff --git a/lib/settings/view/buttons.dart b/lib/settings/view/buttons.dart index 0dea11f..4a15b41 100644 --- a/lib/settings/view/buttons.dart +++ b/lib/settings/view/buttons.dart @@ -20,12 +20,16 @@ class OkButton extends StatelessWidget { class CancelButton extends StatelessWidget { const CancelButton({ super.key, + this.onPressed, }); + final void Function()? onPressed; + @override Widget build(BuildContext context) { return TextButton( onPressed: () { + onPressed?.call(); Navigator.of(context).pop(); }, child: const Text('Cancel'), diff --git a/lib/settings/view/theme_settings_page.dart b/lib/settings/view/theme_settings_page.dart new file mode 100644 index 0000000..0bb746b --- /dev/null +++ b/lib/settings/view/theme_settings_page.dart @@ -0,0 +1,261 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/view/buttons.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/shared/extensions/enum.dart'; + +class ThemeSettingsPage extends HookConsumerWidget { + const ThemeSettingsPage({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final themeSettings = appSettings.themeSettings; + final primaryColor = Theme.of(context).colorScheme.primary; + + return SimpleSettingsPage( + title: const Text('Theme Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + tiles: [ + // choose system , light or dark theme + SettingsTile.navigation( + title: const Text('Theme Mode'), + description: Text.rich( + TextSpan( + text: themeSettings.themeMode == ThemeMode.system + ? 'Using mode from ' + : 'Using ', + children: [ + TextSpan( + text: themeSettings.themeMode.pascalCase, + style: TextStyle( + color: primaryColor, + ), + ), + if (themeSettings.themeMode != ThemeMode.system) + const TextSpan(text: ' mode'), + ], + ), + ), + leading: const Icon(Icons.color_lens), + trailing: themeSettings.themeMode == ThemeMode.system + ? const Icon(Icons.auto_awesome) + : themeSettings.themeMode == ThemeMode.light + ? const Icon(Icons.light_mode) + : const Icon(Icons.dark_mode), + onPressed: (context) async { + final themeMode = await showDialog( + context: context, + builder: (context) { + return ModeSelectionDialog( + themeMode: themeSettings.themeMode, + ); + }, + ); + if (themeMode != null) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.themeSettings( + themeMode: themeMode, + ), + ); + } + }, + ), + + // high contrast mode + SettingsTile.switchTile( + leading: themeSettings.highContrast + ? const Icon(Icons.accessibility) + : const Icon(Icons.accessibility_new_outlined), + initialValue: themeSettings.highContrast, + title: const Text('High Contrast Mode'), + description: const Text( + 'Increase the contrast between the background and the text', + ), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.themeSettings( + highContrast: value, + ), + ); + }, + ), + + // use material theme from system + SettingsTile.switchTile( + initialValue: themeSettings.useMaterialThemeFromSystem, + title: Platform.isAndroid + ? const Text('Use Material You') + : const Text('Material Theme from System'), + description: const Text( + 'Use the system theme colors for the app', + ), + leading: themeSettings.useMaterialThemeFromSystem + ? const Icon(Icons.auto_awesome) + : const Icon(Icons.auto_fix_off), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.themeSettings( + useMaterialThemeFromSystem: value, + ), + ); + }, + ), + + // TODO choose the primary color + // SettingsTile.navigation( + // title: const Text('Primary Color'), + // description: const Text( + // 'Choose the primary color for the app', + // ), + // leading: const Icon(Icons.colorize), + // trailing: Icon( + // Icons.circle, + // color: themeSettings.customThemeColor.toColor(), + // ), + // onPressed: (context) async { + // final selectedColor = await showDialog( + // context: context, + // builder: (context) { + // return SimpleDialog( + // title: const Text('Select Primary Color'), + // children: [ + // for (final color in Colors.primaries) + // SimpleDialogOption( + // onPressed: () { + // Navigator.pop(context, color); + // }, + // child: Container( + // color: color, + // height: 48, + // ), + // ), + // ], + // ); + // }, + // ); + // if (selectedColor != null) { + // ref.read(appSettingsProvider.notifier).update( + // appSettings.copyWith.themeSettings( + // customThemeColor: selectedColor.toHexString(), + // ), + // ); + // } + // }, + // ), + + // use theme throughout the app when playing item + SettingsTile.switchTile( + initialValue: themeSettings.useCurrentPlayerThemeThroughoutApp, + title: const Text('Adapt theme from currently playing item'), + description: const Text( + 'Use the theme colors from the currently playing item for the app', + ), + leading: themeSettings.useCurrentPlayerThemeThroughoutApp + ? const Icon(Icons.auto_fix_high) + : const Icon(Icons.auto_fix_off), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.themeSettings( + useCurrentPlayerThemeThroughoutApp: value, + ), + ); + }, + ), + + SettingsTile.switchTile( + initialValue: themeSettings.useMaterialThemeOnItemPage, + title: const Text('Adaptive Theme on Item Page'), + description: const Text( + 'get fancy with the colors on the item page at the cost of some performance', + ), + leading: themeSettings.useMaterialThemeOnItemPage + ? const Icon(Icons.auto_fix_high) + : const Icon(Icons.auto_fix_off), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.themeSettings( + useMaterialThemeOnItemPage: value, + ), + ); + }, + ), + ], + ), + ], + ); + } +} + +extension ColorExtension on Color { + String toHexString() { + return '#${value.toRadixString(16).substring(2)}'; + } +} + +extension StringExtension on String { + Color toColor() { + return Color(int.parse('0xff$substring(1)')); + } +} + +class ModeSelectionDialog extends HookConsumerWidget { + final ThemeMode themeMode; + + const ModeSelectionDialog({ + super.key, + required this.themeMode, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedTheme = useState(themeMode); + // a wrap of chips to show the available modes with icons + return AlertDialog( + title: const Text('Select Theme Mode'), + content: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: ThemeMode.values + .map( + (mode) => ChoiceChip( + avatar: switch (mode) { + ThemeMode.system => const Icon(Icons.auto_awesome), + ThemeMode.light => const Icon(Icons.light_mode), + ThemeMode.dark => const Icon(Icons.dark_mode), + }, + showCheckmark: false, + label: Text(mode.pascalCase), + selected: mode == selectedTheme.value, + onSelected: (selected) { + if (selected) { + selectedTheme.value = mode; + } + }, + ), + ) + .toList(), + ), + actions: [ + CancelButton(), + OkButton( + onPressed: () { + Navigator.pop(context, selectedTheme.value); + }, + ), + ], + ); + } +} diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index 692d3b3..db6c1b3 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -17,7 +17,7 @@ import 'package:vaani/router/router.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/home_shelf.dart'; -import 'package:vaani/theme/theme_from_cover_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; /// A shelf that displays books on the home page class BookHomeShelf extends HookConsumerWidget { diff --git a/lib/theme/dark.dart b/lib/theme/dark.dart deleted file mode 100644 index 5473632..0000000 --- a/lib/theme/dark.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:vaani/theme/theme.dart'; - -final ThemeData darkTheme = ThemeData( - brightness: Brightness.dark, - colorScheme: ColorScheme.fromSeed( - seedColor: brandColor, - brightness: Brightness.dark, - ), -); diff --git a/lib/theme/light.dart b/lib/theme/light.dart deleted file mode 100644 index aa6ba4d..0000000 --- a/lib/theme/light.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:vaani/theme/theme.dart'; - -final ThemeData lightTheme = ThemeData( - brightness: Brightness.light, - colorScheme: ColorScheme.fromSeed( - seedColor: brandColor, - brightness: Brightness.light, - ), -); diff --git a/lib/theme/providers/system_theme_provider.dart b/lib/theme/providers/system_theme_provider.dart new file mode 100644 index 0000000..a38b9c6 --- /dev/null +++ b/lib/theme/providers/system_theme_provider.dart @@ -0,0 +1,73 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; +import 'package:material_color_utilities/material_color_utilities.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'system_theme_provider.g.dart'; + +final _logger = Logger('SystemThemeProvider'); + +/// copied from [DynamicColorBuilder] +@Riverpod(keepAlive: true) +FutureOr<(ColorScheme light, ColorScheme dark)?> systemTheme( + SystemThemeRef ref, { + bool highContrast = false, +}) async { + _logger.fine('Generating system theme'); + ColorScheme? schemeLight; + ColorScheme? schemeDark; + // Platform messages may fail, so we use a try/catch PlatformException. + try { + CorePalette? corePalette = await DynamicColorPlugin.getCorePalette(); + + if (corePalette != null) { + _logger.fine('dynamic_color: Core palette detected.'); + schemeLight = corePalette.toColorScheme(brightness: Brightness.light); + schemeDark = corePalette.toColorScheme(brightness: Brightness.dark); + } + } on PlatformException { + _logger.warning('dynamic_color: Failed to obtain core palette.'); + } + + if (schemeLight == null || schemeDark == null) { + try { + final Color? accentColor = await DynamicColorPlugin.getAccentColor(); + + if (accentColor != null) { + _logger.fine('dynamic_color: Accent color detected.'); + schemeLight = ColorScheme.fromSeed( + seedColor: accentColor, + brightness: Brightness.light, + ); + schemeDark = ColorScheme.fromSeed( + seedColor: accentColor, + brightness: Brightness.dark, + ); + } + } on PlatformException { + _logger.warning('dynamic_color: Failed to obtain accent color.'); + } + } + + if (schemeLight == null || schemeDark == null) { + _logger + .warning('dynamic_color: Dynamic color not detected on this device.'); + return null; + } + // set high contrast theme + if (highContrast) { + schemeLight = schemeLight + .copyWith( + surface: Colors.white, + ) + .harmonized(); + schemeDark = schemeDark + .copyWith( + surface: Colors.black, + ) + .harmonized(); + } + return (schemeLight, schemeDark); +} diff --git a/lib/theme/providers/system_theme_provider.g.dart b/lib/theme/providers/system_theme_provider.g.dart new file mode 100644 index 0000000..1b4b8db --- /dev/null +++ b/lib/theme/providers/system_theme_provider.g.dart @@ -0,0 +1,177 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'system_theme_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$systemThemeHash() => r'0af4a012a2a2b2fa91642a1313515cba02cd3535'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// copied from [DynamicColorBuilder] +/// +/// Copied from [systemTheme]. +@ProviderFor(systemTheme) +const systemThemeProvider = SystemThemeFamily(); + +/// copied from [DynamicColorBuilder] +/// +/// Copied from [systemTheme]. +class SystemThemeFamily + extends Family> { + /// copied from [DynamicColorBuilder] + /// + /// Copied from [systemTheme]. + const SystemThemeFamily(); + + /// copied from [DynamicColorBuilder] + /// + /// Copied from [systemTheme]. + SystemThemeProvider call({ + bool highContrast = false, + }) { + return SystemThemeProvider( + highContrast: highContrast, + ); + } + + @override + SystemThemeProvider getProviderOverride( + covariant SystemThemeProvider provider, + ) { + return call( + highContrast: provider.highContrast, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'systemThemeProvider'; +} + +/// copied from [DynamicColorBuilder] +/// +/// Copied from [systemTheme]. +class SystemThemeProvider + extends FutureProvider<(ColorScheme light, ColorScheme dark)?> { + /// copied from [DynamicColorBuilder] + /// + /// Copied from [systemTheme]. + SystemThemeProvider({ + bool highContrast = false, + }) : this._internal( + (ref) => systemTheme( + ref as SystemThemeRef, + highContrast: highContrast, + ), + from: systemThemeProvider, + name: r'systemThemeProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$systemThemeHash, + dependencies: SystemThemeFamily._dependencies, + allTransitiveDependencies: + SystemThemeFamily._allTransitiveDependencies, + highContrast: highContrast, + ); + + SystemThemeProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.highContrast, + }) : super.internal(); + + final bool highContrast; + + @override + Override overrideWith( + FutureOr<(ColorScheme light, ColorScheme dark)?> Function( + SystemThemeRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: SystemThemeProvider._internal( + (ref) => create(ref as SystemThemeRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + highContrast: highContrast, + ), + ); + } + + @override + FutureProviderElement<(ColorScheme light, ColorScheme dark)?> + createElement() { + return _SystemThemeProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SystemThemeProvider && other.highContrast == highContrast; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, highContrast.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin SystemThemeRef + on FutureProviderRef<(ColorScheme light, ColorScheme dark)?> { + /// The parameter `highContrast` of this provider. + bool get highContrast; +} + +class _SystemThemeProviderElement + extends FutureProviderElement<(ColorScheme light, ColorScheme dark)?> + with SystemThemeRef { + _SystemThemeProviderElement(super.provider); + + @override + bool get highContrast => (origin as SystemThemeProvider).highContrast; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/theme/theme_from_cover_provider.dart b/lib/theme/providers/theme_from_cover_provider.dart similarity index 81% rename from lib/theme/theme_from_cover_provider.dart rename to lib/theme/providers/theme_from_cover_provider.dart index a535f2f..a61a381 100644 --- a/lib/theme/theme_from_cover_provider.dart +++ b/lib/theme/providers/theme_from_cover_provider.dart @@ -1,3 +1,4 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:logging/logging.dart'; @@ -13,15 +14,25 @@ Future> themeFromCover( ThemeFromCoverRef ref, ImageProvider img, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) async { // ! add deliberate delay to simulate a long running task as it interferes with other animations await Future.delayed(500.ms); _logger.fine('Generating color scheme from cover image'); - return ColorScheme.fromImageProvider( + var theme = await ColorScheme.fromImageProvider( provider: img, brightness: brightness, ); + // set high contrast theme + if (highContrast) { + theme = theme + .copyWith( + surface: brightness == Brightness.light ? Colors.white : Colors.black, + ) + .harmonized(); + } + return theme; // TODO isolate is not working // see https://github.com/flutter/flutter/issues/119207 // use isolate to generate the color scheme @@ -50,14 +61,18 @@ FutureOr themeOfLibraryItem( ThemeOfLibraryItemRef ref, String? itemId, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) async { if (itemId == null) { return null; } final coverImage = await ref.watch(coverImageProvider(itemId).future); final val = await ref.watch( - themeFromCoverProvider(MemoryImage(coverImage), brightness: brightness) - .future, + themeFromCoverProvider( + MemoryImage(coverImage), + brightness: brightness, + highContrast: highContrast, + ).future, ); return val; // coverImage.when( diff --git a/lib/theme/theme_from_cover_provider.g.dart b/lib/theme/providers/theme_from_cover_provider.g.dart similarity index 86% rename from lib/theme/theme_from_cover_provider.g.dart rename to lib/theme/providers/theme_from_cover_provider.g.dart index 61a86a9..07b5790 100644 --- a/lib/theme/theme_from_cover_provider.g.dart +++ b/lib/theme/providers/theme_from_cover_provider.g.dart @@ -6,7 +6,7 @@ part of 'theme_from_cover_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$themeFromCoverHash() => r'a549513a0dcdff76be94488baf38a8b886ce63eb'; +String _$themeFromCoverHash() => r'f656614e2d4851acdfa16d249b3198ae0e1d6d6f'; /// Copied from Dart SDK class _SystemHash { @@ -42,10 +42,12 @@ class ThemeFromCoverFamily extends Family>> { ThemeFromCoverProvider call( ImageProvider img, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) { return ThemeFromCoverProvider( img, brightness: brightness, + highContrast: highContrast, ); } @@ -56,6 +58,7 @@ class ThemeFromCoverFamily extends Family>> { return call( provider.img, brightness: provider.brightness, + highContrast: provider.highContrast, ); } @@ -80,11 +83,13 @@ class ThemeFromCoverProvider extends FutureProvider> { ThemeFromCoverProvider( ImageProvider img, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) : this._internal( (ref) => themeFromCover( ref as ThemeFromCoverRef, img, brightness: brightness, + highContrast: highContrast, ), from: themeFromCoverProvider, name: r'themeFromCoverProvider', @@ -97,6 +102,7 @@ class ThemeFromCoverProvider extends FutureProvider> { ThemeFromCoverFamily._allTransitiveDependencies, img: img, brightness: brightness, + highContrast: highContrast, ); ThemeFromCoverProvider._internal( @@ -108,10 +114,12 @@ class ThemeFromCoverProvider extends FutureProvider> { required super.from, required this.img, required this.brightness, + required this.highContrast, }) : super.internal(); final ImageProvider img; final Brightness brightness; + final bool highContrast; @override Override overrideWith( @@ -129,6 +137,7 @@ class ThemeFromCoverProvider extends FutureProvider> { debugGetCreateSourceHash: null, img: img, brightness: brightness, + highContrast: highContrast, ), ); } @@ -142,7 +151,8 @@ class ThemeFromCoverProvider extends FutureProvider> { bool operator ==(Object other) { return other is ThemeFromCoverProvider && other.img == img && - other.brightness == brightness; + other.brightness == brightness && + other.highContrast == highContrast; } @override @@ -150,6 +160,7 @@ class ThemeFromCoverProvider extends FutureProvider> { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, img.hashCode); hash = _SystemHash.combine(hash, brightness.hashCode); + hash = _SystemHash.combine(hash, highContrast.hashCode); return _SystemHash.finish(hash); } @@ -161,6 +172,9 @@ mixin ThemeFromCoverRef on FutureProviderRef> { /// The parameter `brightness` of this provider. Brightness get brightness; + + /// The parameter `highContrast` of this provider. + bool get highContrast; } class _ThemeFromCoverProviderElement @@ -172,10 +186,12 @@ class _ThemeFromCoverProviderElement ImageProvider get img => (origin as ThemeFromCoverProvider).img; @override Brightness get brightness => (origin as ThemeFromCoverProvider).brightness; + @override + bool get highContrast => (origin as ThemeFromCoverProvider).highContrast; } String _$themeOfLibraryItemHash() => - r'a1d0e5d81f4debe88d5a6ce46c3af28623ad4273'; + r'b2677daf31a6a53f3f237e5204c62dff5ec43171'; /// See also [themeOfLibraryItem]. @ProviderFor(themeOfLibraryItem) @@ -190,10 +206,12 @@ class ThemeOfLibraryItemFamily extends Family> { ThemeOfLibraryItemProvider call( String? itemId, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) { return ThemeOfLibraryItemProvider( itemId, brightness: brightness, + highContrast: highContrast, ); } @@ -204,6 +222,7 @@ class ThemeOfLibraryItemFamily extends Family> { return call( provider.itemId, brightness: provider.brightness, + highContrast: provider.highContrast, ); } @@ -228,11 +247,13 @@ class ThemeOfLibraryItemProvider extends FutureProvider { ThemeOfLibraryItemProvider( String? itemId, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) : this._internal( (ref) => themeOfLibraryItem( ref as ThemeOfLibraryItemRef, itemId, brightness: brightness, + highContrast: highContrast, ), from: themeOfLibraryItemProvider, name: r'themeOfLibraryItemProvider', @@ -245,6 +266,7 @@ class ThemeOfLibraryItemProvider extends FutureProvider { ThemeOfLibraryItemFamily._allTransitiveDependencies, itemId: itemId, brightness: brightness, + highContrast: highContrast, ); ThemeOfLibraryItemProvider._internal( @@ -256,10 +278,12 @@ class ThemeOfLibraryItemProvider extends FutureProvider { required super.from, required this.itemId, required this.brightness, + required this.highContrast, }) : super.internal(); final String? itemId; final Brightness brightness; + final bool highContrast; @override Override overrideWith( @@ -276,6 +300,7 @@ class ThemeOfLibraryItemProvider extends FutureProvider { debugGetCreateSourceHash: null, itemId: itemId, brightness: brightness, + highContrast: highContrast, ), ); } @@ -289,7 +314,8 @@ class ThemeOfLibraryItemProvider extends FutureProvider { bool operator ==(Object other) { return other is ThemeOfLibraryItemProvider && other.itemId == itemId && - other.brightness == brightness; + other.brightness == brightness && + other.highContrast == highContrast; } @override @@ -297,6 +323,7 @@ class ThemeOfLibraryItemProvider extends FutureProvider { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, itemId.hashCode); hash = _SystemHash.combine(hash, brightness.hashCode); + hash = _SystemHash.combine(hash, highContrast.hashCode); return _SystemHash.finish(hash); } @@ -308,6 +335,9 @@ mixin ThemeOfLibraryItemRef on FutureProviderRef { /// The parameter `brightness` of this provider. Brightness get brightness; + + /// The parameter `highContrast` of this provider. + bool get highContrast; } class _ThemeOfLibraryItemProviderElement @@ -319,6 +349,8 @@ class _ThemeOfLibraryItemProviderElement @override Brightness get brightness => (origin as ThemeOfLibraryItemProvider).brightness; + @override + bool get highContrast => (origin as ThemeOfLibraryItemProvider).highContrast; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 72e81b1..35f4ad8 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,9 +1,15 @@ -import 'dart:ui'; - -export 'dark.dart'; -export 'light.dart'; - +import 'package:flutter/material.dart'; // brand color rgb(49, 27, 146) rgb(96, 76, 236) const brandColor = Color(0xFF311B92); const brandColorLight = Color(0xFF604CEC); + +final brandLightColorScheme = ColorScheme.fromSeed( + seedColor: brandColor, + brightness: Brightness.light, +); + +final brandDarkColorScheme = ColorScheme.fromSeed( + seedColor: brandColor, + brightness: Brightness.dark, +); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 879195f..606c5a6 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 026cbff..6023074 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color isar_flutter_libs media_kit_libs_linux url_launcher_linux diff --git a/pubspec.lock b/pubspec.lock index 670cc8f..a6ff204 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -390,6 +390,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" easy_stepper: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1b4fb1c..6366275 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: cupertino_icons: ^1.0.6 device_info_plus: ^10.1.0 duration_picker: ^1.2.0 + dynamic_color: ^1.7.0 easy_stepper: ^0.8.4 file_picker: ^8.1.2 flutter: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d043b0b..de21556 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9839b50..13d504d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color isar_flutter_libs media_kit_libs_windows_audio permission_handler_windows