diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index 7bb635ad6..5aadc464e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -32,6 +32,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { "canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context), "canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT), "canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP), + "isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S), "showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O), "supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q), ) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0dc847cfa..85c9de665 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -723,6 +723,7 @@ "settingsSectionDisplay": "Display", "settingsThemeBrightness": "Theme", "settingsThemeColorHighlights": "Color highlights", + "settingsThemeEnableDynamicColor": "Dynamic color", "settingsDisplayRefreshRateModeTile": "Display refresh rate", "settingsDisplayRefreshRateModeTitle": "Refresh Rate", diff --git a/lib/model/device.dart b/lib/model/device.dart index ab288f972..21f81b5a7 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -6,7 +6,7 @@ final Device device = Device._private(); class Device { late final String _userAgent; late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis; - late final bool _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; + late final bool _isDynamicColorAvailable, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; String get userAgent => _userAgent; @@ -18,6 +18,8 @@ class Device { bool get canRenderFlagEmojis => _canRenderFlagEmojis; + bool get isDynamicColorAvailable => _isDynamicColorAvailable; + bool get showPinShortcutFeedback => _showPinShortcutFeedback; bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode; @@ -33,6 +35,7 @@ class Device { _canPinShortcut = capabilities['canPinShortcut'] ?? false; _canPrint = capabilities['canPrint'] ?? false; _canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false; + _isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false; _showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false; _supportEdgeToEdgeUIMode = capabilities['supportEdgeToEdgeUIMode'] ?? false; } diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index ac2fdfc35..9f1be6762 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -17,11 +17,15 @@ class SettingsDefaults { static const canUseAnalysisService = true; static const isInstalledAppAccessAllowed = false; static const isErrorReportingAllowed = false; + static const tileLayout = TileLayout.grid; + static const entryRenamingPattern = '<${DateNamingProcessor.key}, yyyyMMdd-HHmmss> <${NameNamingProcessor.key}>'; + + // display static const displayRefreshRateMode = DisplayRefreshRateMode.auto; static const themeBrightness = AvesThemeBrightness.system; static const themeColorMode = AvesThemeColorMode.polychrome; - static const tileLayout = TileLayout.grid; - static const entryRenamingPattern = '<${DateNamingProcessor.key}, yyyyMMdd-HHmmss> <${NameNamingProcessor.key}>'; + static const enableDynamicColor = false; + static const enableBlurEffect = true; // `enableBlurEffect` has a contextual default value // navigation static const mustBackTwiceToExit = true; @@ -79,7 +83,6 @@ class SettingsDefaults { static const showOverlayInfo = true; static const showOverlayShootingDetails = false; static const showOverlayThumbnailPreview = false; - static const enableOverlayBlurEffect = true; // `enableOverlayBlurEffect` has a contextual default value static const viewerUseCutout = true; static const viewerMaxBrightness = false; static const enableMotionPhotoAutoPlay = false; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 6c0617863..ab561ac98 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -42,15 +42,19 @@ class Settings extends ChangeNotifier { static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed'; static const isErrorReportingAllowedKey = 'is_crashlytics_enabled'; static const localeKey = 'locale'; - static const displayRefreshRateModeKey = 'display_refresh_rate_mode'; - static const themeBrightnessKey = 'theme_brightness'; - static const themeColorModeKey = 'theme_color_mode'; static const catalogTimeZoneKey = 'catalog_time_zone'; static const tileExtentPrefixKey = 'tile_extent_'; static const tileLayoutPrefixKey = 'tile_layout_'; static const entryRenamingPatternKey = 'entry_renaming_pattern'; static const topEntryIdsKey = 'top_entry_ids'; + // display + static const displayRefreshRateModeKey = 'display_refresh_rate_mode'; + static const themeBrightnessKey = 'theme_brightness'; + static const themeColorModeKey = 'theme_color_mode'; + static const enableDynamicColorKey = 'dynamic_color'; + static const enableBlurEffectKey = 'enable_overlay_blur_effect'; + // navigation static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; static const keepScreenOnKey = 'keep_screen_on'; @@ -92,7 +96,6 @@ class Settings extends ChangeNotifier { static const showOverlayInfoKey = 'show_overlay_info'; static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details'; static const showOverlayThumbnailPreviewKey = 'show_overlay_thumbnail_preview'; - static const enableOverlayBlurEffectKey = 'enable_overlay_blur_effect'; static const viewerUseCutoutKey = 'viewer_use_cutout'; static const viewerMaxBrightnessKey = 'viewer_max_brightness'; static const enableMotionPhotoAutoPlayKey = 'motion_photo_auto_play'; @@ -161,7 +164,7 @@ class Settings extends ChangeNotifier { Future setContextualDefaults() async { // performance final performanceClass = await deviceService.getPerformanceClass(); - enableOverlayBlurEffect = performanceClass >= 29; + enableBlurEffect = performanceClass >= 29; // availability final defaultMapStyle = mobileServices.defaultMapStyle; @@ -249,18 +252,6 @@ class Settings extends ChangeNotifier { return _appliedLocale!; } - DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values); - - set displayRefreshRateMode(DisplayRefreshRateMode newValue) => setAndNotify(displayRefreshRateModeKey, newValue.toString()); - - AvesThemeBrightness get themeBrightness => getEnumOrDefault(themeBrightnessKey, SettingsDefaults.themeBrightness, AvesThemeBrightness.values); - - set themeBrightness(AvesThemeBrightness newValue) => setAndNotify(themeBrightnessKey, newValue.toString()); - - AvesThemeColorMode get themeColorMode => getEnumOrDefault(themeColorModeKey, SettingsDefaults.themeColorMode, AvesThemeColorMode.values); - - set themeColorMode(AvesThemeColorMode newValue) => setAndNotify(themeColorModeKey, newValue.toString()); - String get catalogTimeZone => getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); @@ -281,6 +272,28 @@ class Settings extends ChangeNotifier { set topEntryIds(List? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList()); + // display + + DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values); + + set displayRefreshRateMode(DisplayRefreshRateMode newValue) => setAndNotify(displayRefreshRateModeKey, newValue.toString()); + + AvesThemeBrightness get themeBrightness => getEnumOrDefault(themeBrightnessKey, SettingsDefaults.themeBrightness, AvesThemeBrightness.values); + + set themeBrightness(AvesThemeBrightness newValue) => setAndNotify(themeBrightnessKey, newValue.toString()); + + AvesThemeColorMode get themeColorMode => getEnumOrDefault(themeColorModeKey, SettingsDefaults.themeColorMode, AvesThemeColorMode.values); + + set themeColorMode(AvesThemeColorMode newValue) => setAndNotify(themeColorModeKey, newValue.toString()); + + bool get enableDynamicColor => getBoolOrDefault(enableDynamicColorKey, SettingsDefaults.enableDynamicColor); + + set enableDynamicColor(bool newValue) => setAndNotify(enableDynamicColorKey, newValue); + + bool get enableBlurEffect => getBoolOrDefault(enableBlurEffectKey, SettingsDefaults.enableBlurEffect); + + set enableBlurEffect(bool newValue) => setAndNotify(enableBlurEffectKey, newValue); + // navigation bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, SettingsDefaults.mustBackTwiceToExit); @@ -441,10 +454,6 @@ class Settings extends ChangeNotifier { set showOverlayThumbnailPreview(bool newValue) => setAndNotify(showOverlayThumbnailPreviewKey, newValue); - bool get enableOverlayBlurEffect => getBoolOrDefault(enableOverlayBlurEffectKey, SettingsDefaults.enableOverlayBlurEffect); - - set enableOverlayBlurEffect(bool newValue) => setAndNotify(enableOverlayBlurEffectKey, newValue); - bool get viewerUseCutout => getBoolOrDefault(viewerUseCutoutKey, SettingsDefaults.viewerUseCutout); set viewerUseCutout(bool newValue) => setAndNotify(viewerUseCutoutKey, newValue); @@ -695,6 +704,8 @@ class Settings extends ChangeNotifier { break; case isInstalledAppAccessAllowedKey: case isErrorReportingAllowedKey: + case enableDynamicColorKey: + case enableBlurEffectKey: case showBottomNavigationBarKey: case mustBackTwiceToExitKey: case confirmDeleteForeverKey: @@ -713,7 +724,6 @@ class Settings extends ChangeNotifier { case showOverlayInfoKey: case showOverlayShootingDetailsKey: case showOverlayThumbnailPreviewKey: - case enableOverlayBlurEffectKey: case viewerUseCutoutKey: case viewerMaxBrightnessKey: case enableMotionPhotoAutoPlayKey: diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index 034874289..0cb4e1fd0 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; class Themes { - static const _accentColor = Colors.indigoAccent; + static const defaultAccent = Colors.indigoAccent; static const _tooltipTheme = TooltipThemeData( verticalOffset: 32, @@ -19,10 +19,10 @@ class Themes { fontFeatures: [FontFeature.enable('smcp')], ); - static const _snackBarTheme = SnackBarThemeData( - actionTextColor: _accentColor, - behavior: SnackBarBehavior.floating, - ); + static SnackBarThemeData _snackBarTheme(Color accentColor) => SnackBarThemeData( + actionTextColor: accentColor, + behavior: SnackBarBehavior.floating, + ); static final _typography = Typography.material2018(platform: TargetPlatform.android); @@ -35,49 +35,49 @@ class Themes { static const _lightSecondLayer = Color(0xFFF5F5F5); // aka `Colors.grey[100]` static const _lightThirdLayer = Color(0xFFEEEEEE); // aka `Colors.grey[200]` - static final lightTheme = ThemeData( - colorScheme: ColorScheme.light( - primary: _accentColor, - secondary: _accentColor, - onPrimary: _lightBodyColor, - onSecondary: _lightBodyColor, - ), - brightness: Brightness.light, - // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` - canvasColor: _lightSecondLayer, - scaffoldBackgroundColor: _lightFirstLayer, - // `cardColor` is used by `ExpansionPanel` - cardColor: _lightSecondLayer, - dialogBackgroundColor: _lightSecondLayer, - indicatorColor: _accentColor, - toggleableActiveColor: _accentColor, - typography: _typography, - appBarTheme: AppBarTheme( - backgroundColor: _lightFirstLayer, - // `foregroundColor` is used by icons - foregroundColor: _lightActionIconColor, - // `titleTextStyle.color` is used by text - titleTextStyle: _appBarTitleTextStyle.copyWith(color: _lightTitleColor), - systemOverlayStyle: SystemUiOverlayStyle.dark, - ), - listTileTheme: const ListTileThemeData( - iconColor: _lightActionIconColor, - ), - popupMenuTheme: const PopupMenuThemeData( - color: _lightSecondLayer, - ), - snackBarTheme: _snackBarTheme, - tabBarTheme: TabBarTheme( - labelColor: _lightTitleColor, - unselectedLabelColor: Colors.black54, - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - primary: _lightLabelColor, - ), - ), - tooltipTheme: _tooltipTheme, - ); + static ThemeData lightTheme(Color accentColor) => ThemeData( + colorScheme: ColorScheme.light( + primary: accentColor, + secondary: accentColor, + onPrimary: _lightBodyColor, + onSecondary: _lightBodyColor, + ), + brightness: Brightness.light, + // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` + canvasColor: _lightSecondLayer, + scaffoldBackgroundColor: _lightFirstLayer, + // `cardColor` is used by `ExpansionPanel` + cardColor: _lightSecondLayer, + dialogBackgroundColor: _lightSecondLayer, + indicatorColor: accentColor, + toggleableActiveColor: accentColor, + typography: _typography, + appBarTheme: AppBarTheme( + backgroundColor: _lightFirstLayer, + // `foregroundColor` is used by icons + foregroundColor: _lightActionIconColor, + // `titleTextStyle.color` is used by text + titleTextStyle: _appBarTitleTextStyle.copyWith(color: _lightTitleColor), + systemOverlayStyle: SystemUiOverlayStyle.dark, + ), + listTileTheme: const ListTileThemeData( + iconColor: _lightActionIconColor, + ), + popupMenuTheme: const PopupMenuThemeData( + color: _lightSecondLayer, + ), + snackBarTheme: _snackBarTheme(accentColor), + tabBarTheme: TabBarTheme( + labelColor: _lightTitleColor, + unselectedLabelColor: Colors.black54, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + primary: _lightLabelColor, + ), + ), + tooltipTheme: _tooltipTheme, + ); static final _darkThemeTypo = _typography.white; static final _darkTitleColor = _darkThemeTypo.titleMedium!.color!; @@ -87,71 +87,74 @@ class Themes { static const _darkSecondLayer = Color(0xFF363636); static const _darkThirdLayer = Color(0xFF424242); // aka `Colors.grey[800]` - static final darkTheme = ThemeData( - colorScheme: ColorScheme.dark( - primary: _accentColor, - secondary: _accentColor, - // surface color is used by the date/time pickers - surface: Colors.grey.shade800, - onPrimary: _darkBodyColor, - onSecondary: _darkBodyColor, - ), - brightness: Brightness.dark, - // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` - canvasColor: _darkSecondLayer, - scaffoldBackgroundColor: _darkFirstLayer, - // `cardColor` is used by `ExpansionPanel` - cardColor: _darkSecondLayer, - dialogBackgroundColor: _darkSecondLayer, - indicatorColor: _accentColor, - toggleableActiveColor: _accentColor, - typography: _typography, - appBarTheme: AppBarTheme( - backgroundColor: _darkFirstLayer, - // `foregroundColor` is used by icons - foregroundColor: _darkTitleColor, - // `titleTextStyle.color` is used by text - titleTextStyle: _appBarTitleTextStyle.copyWith(color: _darkTitleColor), - systemOverlayStyle: SystemUiOverlayStyle.light, - ), - popupMenuTheme: const PopupMenuThemeData( - color: _darkSecondLayer, - ), - snackBarTheme: _snackBarTheme.copyWith( - backgroundColor: Colors.grey.shade800, - contentTextStyle: TextStyle( - color: _darkBodyColor, - ), - ), - tabBarTheme: TabBarTheme( - labelColor: _darkTitleColor, - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - primary: _darkLabelColor, - ), - ), - tooltipTheme: _tooltipTheme, - ); + static ThemeData darkTheme(Color accentColor) => ThemeData( + colorScheme: ColorScheme.dark( + primary: accentColor, + secondary: accentColor, + // surface color is used by the date/time pickers + surface: Colors.grey.shade800, + onPrimary: _darkBodyColor, + onSecondary: _darkBodyColor, + ), + brightness: Brightness.dark, + // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` + canvasColor: _darkSecondLayer, + scaffoldBackgroundColor: _darkFirstLayer, + // `cardColor` is used by `ExpansionPanel` + cardColor: _darkSecondLayer, + dialogBackgroundColor: _darkSecondLayer, + indicatorColor: accentColor, + toggleableActiveColor: accentColor, + typography: _typography, + appBarTheme: AppBarTheme( + backgroundColor: _darkFirstLayer, + // `foregroundColor` is used by icons + foregroundColor: _darkTitleColor, + // `titleTextStyle.color` is used by text + titleTextStyle: _appBarTitleTextStyle.copyWith(color: _darkTitleColor), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + popupMenuTheme: const PopupMenuThemeData( + color: _darkSecondLayer, + ), + snackBarTheme: _snackBarTheme(accentColor).copyWith( + backgroundColor: Colors.grey.shade800, + contentTextStyle: TextStyle( + color: _darkBodyColor, + ), + ), + tabBarTheme: TabBarTheme( + labelColor: _darkTitleColor, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + primary: _darkLabelColor, + ), + ), + tooltipTheme: _tooltipTheme, + ); static const _blackFirstLayer = Colors.black; static const _blackSecondLayer = Color(0xFF212121); // aka `Colors.grey[900]` static const _blackThirdLayer = Color(0xFF303030); // aka `Colors.grey[850]` - static final blackTheme = darkTheme.copyWith( - // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` - canvasColor: _blackSecondLayer, - scaffoldBackgroundColor: _blackFirstLayer, - // `cardColor` is used by `ExpansionPanel` - cardColor: _blackSecondLayer, - dialogBackgroundColor: _blackSecondLayer, - appBarTheme: darkTheme.appBarTheme.copyWith( - backgroundColor: _blackFirstLayer, - ), - popupMenuTheme: darkTheme.popupMenuTheme.copyWith( - color: _blackSecondLayer, - ), - ); + static ThemeData blackTheme(Color accentColor) { + final baseTheme = darkTheme(accentColor); + return baseTheme.copyWith( + // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` + canvasColor: _blackSecondLayer, + scaffoldBackgroundColor: _blackFirstLayer, + // `cardColor` is used by `ExpansionPanel` + cardColor: _blackSecondLayer, + dialogBackgroundColor: _blackSecondLayer, + appBarTheme: baseTheme.appBarTheme.copyWith( + backgroundColor: _blackFirstLayer, + ), + popupMenuTheme: baseTheme.popupMenuTheme.copyWith( + color: _blackSecondLayer, + ), + ); + } static Color overlayBackgroundColor({ required Brightness brightness, diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 443e47935..ec9e2a8c2 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -108,6 +108,11 @@ class Constants { licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/device_info_plus/device_info_plus/LICENSE', sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus', ), + Dependency( + name: 'Dynamic Color', + license: 'BSD 3-Clause', + sourceUrl: 'https://github.com/material-foundation/material-dynamic-color-flutter', + ), Dependency( name: 'fijkplayer (Aves fork)', license: 'MIT', @@ -337,6 +342,12 @@ class Constants { license: 'Apache 2.0', sourceUrl: 'https://github.com/jifalops/dart-latlong', ), + Dependency( + name: 'Material Color Utilities', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart/LICENSE', + sourceUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', + ), Dependency( name: 'Path', license: 'BSD 3-Clause', diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 14a878eac..765eb09da 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -5,6 +5,7 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/app_mode.dart'; import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; import 'package:aves/model/settings/enums/enums.dart'; @@ -30,11 +31,13 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:equatable/equatable.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:overlay_support/overlay_support.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -70,6 +73,7 @@ class AvesApp extends StatefulWidget { class _AvesAppState extends State with WidgetsBindingObserver { final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); late Future _appSetup; + late Future _dynamicColorPaletteLoader; final _mediaStoreSource = MediaStoreSource(); final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: Durations.mediaContentChangeDebounceDelay); final Set changedUris = {}; @@ -89,6 +93,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { super.initState(); EquatableConfig.stringify = true; _appSetup = _setup(); + _dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette(); _mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?)); _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); _analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion()); @@ -120,16 +125,18 @@ class _AvesAppState extends State with WidgetsBindingObserver { : Scaffold( body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), ); - return Selector>( - selector: (context, s) => Tuple3( + return Selector>( + selector: (context, s) => Tuple4( s.locale, s.initialized ? s.accessibilityAnimations.animate : true, - s.initialized ? s.themeBrightness : AvesThemeBrightness.system, + s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness, + s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor, ), builder: (context, s, child) { final settingsLocale = s.item1; final areAnimationsEnabled = s.item2; final themeBrightness = s.item3; + final enableDynamicColor = s.item4; final pageTransitionsTheme = areAnimationsEnabled // Flutter has various page transition implementations for Android: @@ -144,27 +151,42 @@ class _AvesAppState extends State with WidgetsBindingObserver { // strip page transitions used by `MaterialPageRoute` : const DirectPageTransitionsTheme(); - return MaterialApp( - navigatorKey: AvesApp.navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - builder: (context, child) => AvesColorsProvider( - child: Theme( - data: Theme.of(context).copyWith( - pageTransitionsTheme: pageTransitionsTheme, + return FutureBuilder( + future: _dynamicColorPaletteLoader, + builder: (context, snapshot) { + const defaultAccent = Themes.defaultAccent; + Color lightAccent = defaultAccent, darkAccent = defaultAccent; + if (enableDynamicColor) { + // `DynamicColorBuilder` from package `dynamic_color` provides light/dark + // palettes with a primary color from tones too dark/light (40/80), + // so we derive the color with adjusted tones (60/70) + final tonalPalette = snapshot.data?.primary; + lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value); + darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value); + } + return MaterialApp( + navigatorKey: AvesApp.navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + builder: (context, child) => AvesColorsProvider( + child: Theme( + data: Theme.of(context).copyWith( + pageTransitionsTheme: pageTransitionsTheme, + ), + child: child!, + ), ), - child: child!, - ), - ), - onGenerateTitle: (context) => context.l10n.appName, - theme: Themes.lightTheme, - darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme : Themes.darkTheme, - themeMode: themeBrightness.appThemeMode, - locale: settingsLocale, - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - // TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906 - scrollBehavior: StretchMaterialScrollBehavior(), + onGenerateTitle: (context) => context.l10n.appName, + theme: Themes.lightTheme(lightAccent), + darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent) : Themes.darkTheme(darkAccent), + themeMode: themeBrightness.appThemeMode, + locale: settingsLocale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + // TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906 + scrollBehavior: StretchMaterialScrollBehavior(), + ); + }, ); }, ); diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart index 8dcadd5f0..6f36bd821 100644 --- a/lib/widgets/common/identity/aves_app_bar.dart +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -206,7 +206,7 @@ class _AvesFloatingBarState extends State with RouteAware { return ValueListenableBuilder( valueListenable: _isBlurAllowedNotifier, builder: (context, isBlurAllowed, child) { - final blurred = isBlurAllowed && context.select((s) => s.enableOverlayBlurEffect); + final blurred = isBlurAllowed && context.select((s) => s.enableBlurEffect); return Container( foregroundDecoration: BoxDecoration( border: Border.all( diff --git a/lib/widgets/common/identity/empty.dart b/lib/widgets/common/identity/empty.dart index 9c40675df..9631c1f09 100644 --- a/lib/widgets/common/identity/empty.dart +++ b/lib/widgets/common/identity/empty.dart @@ -20,7 +20,7 @@ class EmptyContent extends StatelessWidget { @override Widget build(BuildContext context) { - const color = Colors.blueGrey; + final color = Theme.of(context).colorScheme.secondary.withOpacity(.5); return Padding( padding: safeBottom ? EdgeInsets.only( diff --git a/lib/widgets/common/map/buttons/button.dart b/lib/widgets/common/map/buttons/button.dart index 7bd435ce8..60aa8c56f 100644 --- a/lib/widgets/common/map/buttons/button.dart +++ b/lib/widgets/common/map/buttons/button.dart @@ -20,7 +20,7 @@ class MapOverlayButton extends StatelessWidget { @override Widget build(BuildContext context) { - final blurred = settings.enableOverlayBlurEffect; + final blurred = settings.enableBlurEffect; return Selector>( selector: (context, v) => v.scale, builder: (context, scale, child) => ScaleTransition( diff --git a/lib/widgets/common/map/buttons/coordinate_filter.dart b/lib/widgets/common/map/buttons/coordinate_filter.dart index 3056ace31..5c984c4dc 100644 --- a/lib/widgets/common/map/buttons/coordinate_filter.dart +++ b/lib/widgets/common/map/buttons/coordinate_filter.dart @@ -57,7 +57,7 @@ class _OverlayCoordinateFilterChipState extends State { spacing: 16, crossAxisAlignment: WrapCrossAlignment.center, children: [ - const AvesLogo(size: 64), + const AvesLogo(size: 56), Text( context.l10n.appName, style: const TextStyle( diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index 909b7c689..78599c3cf 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/theme_brightness.dart'; @@ -30,8 +31,9 @@ class DisplaySection extends SettingsSection { FutureOr> tiles(BuildContext context) => [ SettingsTileDisplayThemeBrightness(), SettingsTileDisplayThemeColorMode(), - SettingsTileDisplayDisplayRefreshRateMode(), + if (device.isDynamicColorAvailable) SettingsTileDisplayEnableDynamicColor(), SettingsTileDisplayEnableBlurEffect(), + SettingsTileDisplayDisplayRefreshRateMode(), ]; } @@ -62,6 +64,30 @@ class SettingsTileDisplayThemeColorMode extends SettingsTile { ); } +class SettingsTileDisplayEnableDynamicColor extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThemeEnableDynamicColor; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableDynamicColor, + onChanged: (v) => settings.enableDynamicColor = v, + title: title(context), + ); +} + +class SettingsTileDisplayEnableBlurEffect extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerEnableOverlayBlurEffect; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableBlurEffect, + onChanged: (v) => settings.enableBlurEffect = v, + title: title(context), + ); +} + class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { @override String title(BuildContext context) => context.l10n.settingsDisplayRefreshRateModeTile; @@ -76,15 +102,3 @@ class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { dialogTitle: context.l10n.settingsDisplayRefreshRateModeTitle, ); } - -class SettingsTileDisplayEnableBlurEffect extends SettingsTile { - @override - String title(BuildContext context) => context.l10n.settingsViewerEnableOverlayBlurEffect; - - @override - Widget build(BuildContext context) => SettingsSwitchListTile( - selector: (context, s) => s.enableOverlayBlurEffect, - onChanged: (v) => settings.enableOverlayBlurEffect = v, - title: title(context), - ); -} diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index 980ef34ca..523cd5fb9 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -19,7 +19,7 @@ class OverlayButton extends StatelessWidget { @override Widget build(BuildContext context) { final brightness = Theme.of(context).brightness; - final blurred = settings.enableOverlayBlurEffect; + final blurred = settings.enableBlurEffect; return ScaleTransition( scale: scale, child: borderRadius != null @@ -77,7 +77,7 @@ class OverlayTextButton extends StatelessWidget { @override Widget build(BuildContext context) { - final blurred = settings.enableOverlayBlurEffect; + final blurred = settings.enableBlurEffect; final theme = Theme.of(context); return SizeTransition( sizeFactor: scale, diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index d680f431a..6842773c5 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -42,7 +42,7 @@ class ViewerTopOverlay extends StatelessWidget { final viewStateConductor = context.read(); final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry); - final blurred = settings.enableOverlayBlurEffect; + final blurred = settings.enableBlurEffect; final viewInsetsPadding = (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero); return Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index ef244e45d..06d12f9c9 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -39,7 +39,7 @@ class _VideoProgressBarState extends State { @override Widget build(BuildContext context) { - final blurred = settings.enableOverlayBlurEffect; + final blurred = settings.enableBlurEffect; final brightness = Theme.of(context).brightness; final textStyle = TextStyle( shadows: brightness == Brightness.dark ? Constants.embossShadows : null, diff --git a/pubspec.lock b/pubspec.lock index 81ed62b51..f4f7f8d84 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -260,6 +260,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" equatable: dependency: "direct main" description: @@ -589,7 +596,7 @@ packages: source: hosted version: "0.12.11" material_color_utilities: - dependency: transitive + dependency: "direct main" description: name: material_color_utilities url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 5b6dd1b14..b97064374 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: country_code: decorated_icon: device_info_plus: + dynamic_color: equatable: event_bus: expansion_tile_card: @@ -56,6 +57,7 @@ dependencies: get_it: intl: latlong2: + material_color_utilities: material_design_icons_flutter: overlay_support: package_info_plus: diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index 58b35c7ab..cfc6ac14c 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -41,7 +41,7 @@ Future configureAndLaunch() async { ..showOverlayInfo = true ..showOverlayShootingDetails = false ..showOverlayThumbnailPreview = false - ..enableOverlayBlurEffect = true + ..enableBlurEffect = true ..viewerUseCutout = true // info ..infoMapStyle = EntryMapStyle.stamenWatercolor diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart index 8d973b7b3..cde043409 100644 --- a/test_driver/driver_shaders.dart +++ b/test_driver/driver_shaders.dart @@ -29,7 +29,7 @@ Future configureAndLaunch() async { ..showOverlayInfo = true ..showOverlayShootingDetails = true ..showOverlayThumbnailPreview = true - ..enableOverlayBlurEffect = true + ..enableBlurEffect = true ..imageBackground = EntryBackground.checkered // info ..infoMapStyle = EntryMapStyle.googleNormal; diff --git a/untranslated.json b/untranslated.json index 3da227021..439d1ddbe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,6 +1,43 @@ { + "de": [ + "settingsThemeEnableDynamicColor" + ], + "es": [ "settingsShowBottomNavigationBar", - "settingsThumbnailShowTagIcon" + "settingsThumbnailShowTagIcon", + "settingsThemeEnableDynamicColor" + ], + + "fr": [ + "settingsThemeEnableDynamicColor" + ], + + "id": [ + "settingsThemeEnableDynamicColor" + ], + + "it": [ + "settingsThemeEnableDynamicColor" + ], + + "ja": [ + "settingsThemeEnableDynamicColor" + ], + + "ko": [ + "settingsThemeEnableDynamicColor" + ], + + "pt": [ + "settingsThemeEnableDynamicColor" + ], + + "ru": [ + "settingsThemeEnableDynamicColor" + ], + + "zh": [ + "settingsThemeEnableDynamicColor" ] }