From 11f585d0adf384cbabae78a8ec3b4709449cb90f Mon Sep 17 00:00:00 2001 From: dvbthien <89862334+dvbthien@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:30:56 +0700 Subject: [PATCH] refactor(mobile): refactor theme management (#14415) --- mobile/lib/constants/colors.dart | 23 + mobile/lib/main.dart | 36 +- mobile/lib/pages/common/settings.page.dart | 1 + mobile/lib/pages/search/map/map.page.dart | 2 +- .../search/map/map_location_picker.page.dart | 2 +- mobile/lib/providers/theme.provider.dart | 74 ++++ mobile/lib/services/app_settings.service.dart | 2 +- .../color_scheme.dart} | 29 +- mobile/lib/theme/dynamic_theme.dart | 38 ++ .../theme_data.dart} | 407 +++++++----------- .../asset_viewer/motion_photo_button.dart | 2 +- .../widgets/asset_viewer/video_position.dart | 2 +- mobile/lib/widgets/backup/error_chip.dart | 2 +- .../lib/widgets/backup/error_chip_text.dart | 2 +- .../lib/widgets/map/map_theme_override.dart | 11 +- mobile/lib/widgets/map/map_thumbnail.dart | 2 +- .../primary_color_setting.dart | 16 +- .../preference_settings/theme_setting.dart | 2 +- .../modules/map/map_theme_override_test.dart | 12 +- 19 files changed, 343 insertions(+), 322 deletions(-) create mode 100644 mobile/lib/constants/colors.dart create mode 100644 mobile/lib/providers/theme.provider.dart rename mobile/lib/{constants/immich_colors.dart => theme/color_scheme.dart} (80%) create mode 100644 mobile/lib/theme/dynamic_theme.dart rename mobile/lib/{utils/immich_app_theme.dart => theme/theme_data.dart} (58%) diff --git a/mobile/lib/constants/colors.dart b/mobile/lib/constants/colors.dart new file mode 100644 index 0000000000..ade878d6f6 --- /dev/null +++ b/mobile/lib/constants/colors.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +enum ImmichColorPreset { + indigo, + deepPurple, + pink, + red, + orange, + yellow, + lime, + green, + cyan, + slateGray +} + +const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; +const String defaultColorPresetName = "indigo"; + +const Color immichBrandColorLight = Color(0xFF4150AF); +const Color immichBrandColorDark = Color(0xFFACCBFA); +const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); +const Color red400 = Color(0xFFEF5350); +const Color grey200 = Color(0xFFEEEEEE); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7729972aa2..807212fc65 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -4,23 +4,26 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:timezone/data/latest.dart'; +import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/utils/download.dart'; -import 'package:intl/date_symbol_data_local.dart'; -import 'package:timezone/data/latest.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; +import 'package:immich_mobile/providers/locale_provider.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart'; -import 'package:immich_mobile/utils/cache/widgets_binding.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -30,16 +33,15 @@ import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/migration.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:immich_mobile/utils/download.dart'; +import 'package:immich_mobile/utils/cache/widgets_binding.dart'; +import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/theme/dynamic_theme.dart'; void main() async { ImmichWidgetsBinding(); @@ -69,12 +71,12 @@ Future initApp() async { } } - await fetchSystemPalette(); + await DynamicTheme.fetchSystemPalette(); // Initialize Immich Logger Service ImmichLogger(); - var log = Logger("ImmichErrorLogger"); + final log = Logger("ImmichErrorLogger"); FlutterError.onError = (details) { FlutterError.presentError(details); diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index ba3150c046..3cbded1787 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -133,6 +133,7 @@ class _MobileLayout extends StatelessWidget { ).tr(), subtitle: Text( setting.subtitle, + style: context.textTheme.labelLarge, ).tr(), onTap: () => context.pushRoute(SettingsSubRoute(section: setting)), diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index 10fe8de541..52ce13f958 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -264,7 +264,7 @@ class MapPage extends HookConsumerWidget { selectedAssets.value = selected ? selection : {}; } - return MapThemeOveride( + return MapThemeOverride( mapBuilder: (style) => context.isMobile // Single-column ? Scaffold( diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index 2fd1e1ee9e..487de69a1e 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -58,7 +58,7 @@ class MapLocationPickerPage extends HookConsumerWidget { controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng)); } - return MapThemeOveride( + return MapThemeOverride( mapBuilder: (style) => Builder( builder: (ctx) => Scaffold( backgroundColor: ctx.themeData.cardColor, diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart new file mode 100644 index 0000000000..73623bd026 --- /dev/null +++ b/mobile/lib/providers/theme.provider.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/theme/color_scheme.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/theme/dynamic_theme.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; + +final immichThemeModeProvider = StateProvider((ref) { + final themeMode = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.themeMode); + + debugPrint("Current themeMode $themeMode"); + + if (themeMode == ThemeMode.light.name) { + return ThemeMode.light; + } else if (themeMode == ThemeMode.dark.name) { + return ThemeMode.dark; + } else { + return ThemeMode.system; + } +}); + +final immichThemePresetProvider = StateProvider((ref) { + final appSettingsProvider = ref.watch(appSettingsServiceProvider); + final primaryColorPreset = + appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); + + debugPrint("Current theme preset $primaryColorPreset"); + + try { + return ImmichColorPreset.values + .firstWhere((e) => e.name == primaryColorPreset); + } catch (e) { + debugPrint( + "Theme preset $primaryColorPreset not found. Applying default preset.", + ); + appSettingsProvider.setSetting( + AppSettingsEnum.primaryColor, + defaultColorPresetName, + ); + return defaultColorPreset; + } +}); + +final dynamicThemeSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.dynamicTheme); +}); + +final colorfulInterfaceSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.colorfulInterface); +}); + +// Provider for current selected theme +final immichThemeProvider = StateProvider((ref) { + final primaryColorPreset = ref.read(immichThemePresetProvider); + final useSystemColor = ref.watch(dynamicThemeSettingProvider); + final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); + final ImmichTheme? dynamicTheme = DynamicTheme.theme; + final currentTheme = (useSystemColor && dynamicTheme != null) + ? dynamicTheme + : primaryColorPreset.themeOfPreset; + + return useColorfulInterface + ? currentTheme + : decolorizeSurfaces(theme: currentTheme); +}); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 14d800a4ef..c3fde894d5 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -1,4 +1,4 @@ -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/theme/color_scheme.dart similarity index 80% rename from mobile/lib/constants/immich_colors.dart rename to mobile/lib/theme/color_scheme.dart index 847887de8c..c01b7cfa5a 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/theme/color_scheme.dart @@ -1,29 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; -enum ImmichColorPreset { - indigo, - deepPurple, - pink, - red, - orange, - yellow, - lime, - green, - cyan, - slateGray -} - -const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; -const String defaultColorPresetName = "indigo"; - -const Color immichBrandColorLight = Color(0xFF4150AF); -const Color immichBrandColorDark = Color(0xFFACCBFA); -const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); -const Color red400 = Color(0xFFEF5350); -const Color grey200 = Color(0xFFEEEEEE); - -final Map _themePresetsMap = { +final Map _themePresets = { ImmichColorPreset.indigo: ImmichTheme( light: ColorScheme.fromSeed( seedColor: immichBrandColorLight, @@ -110,5 +89,5 @@ final Map _themePresetsMap = { }; extension ImmichColorModeExtension on ImmichColorPreset { - ImmichTheme getTheme() => _themePresetsMap[this]!; + ImmichTheme get themeOfPreset => _themePresets[this]!; } diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart new file mode 100644 index 0000000000..39d6b6ee45 --- /dev/null +++ b/mobile/lib/theme/dynamic_theme.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:dynamic_color/dynamic_color.dart'; + +import 'package:immich_mobile/theme/theme_data.dart'; + +abstract final class DynamicTheme { + DynamicTheme._(); + + static ImmichTheme? _theme; + // Method to fetch dynamic system colors + static Future fetchSystemPalette() async { + try { + final corePalette = await DynamicColorPlugin.getCorePalette(); + if (corePalette != null) { + final primaryColor = corePalette.toColorScheme().primary; + debugPrint('dynamic_color: Core palette detected.'); + + // Some palettes do not generate surface container colors accurately, + // so we regenerate all colors using the primary color + _theme = ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.light, + ), + dark: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.dark, + ), + ); + } + } catch (error) { + debugPrint('dynamic_color: Failed to obtain core palette: $error'); + } + } + + static ImmichTheme? get theme => _theme; + static bool get isAvailable => _theme != null; +} diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/theme/theme_data.dart similarity index 58% rename from mobile/lib/utils/immich_app_theme.dart rename to mobile/lib/theme/theme_data.dart index 2ca4fe3aff..de96e12c5d 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/theme/theme_data.dart @@ -1,11 +1,7 @@ -import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; + import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; class ImmichTheme { final ColorScheme light; @@ -14,104 +10,166 @@ class ImmichTheme { const ImmichTheme({required this.light, required this.dark}); } -ImmichTheme? _immichDynamicTheme; -bool get isDynamicThemeAvailable => _immichDynamicTheme != null; +ThemeData getThemeData({ + required ColorScheme colorScheme, + required Locale locale, +}) { + final isDark = colorScheme.brightness == Brightness.dark; -final immichThemeModeProvider = StateProvider((ref) { - var themeMode = ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.themeMode); - - debugPrint("Current themeMode $themeMode"); - - if (themeMode == "light") { - return ThemeMode.light; - } else if (themeMode == "dark") { - return ThemeMode.dark; - } else { - return ThemeMode.system; - } -}); - -final immichThemePresetProvider = StateProvider((ref) { - var appSettingsProvider = ref.watch(appSettingsServiceProvider); - var primaryColorName = - appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); - - debugPrint("Current theme preset $primaryColorName"); - - try { - return ImmichColorPreset.values - .firstWhere((e) => e.name == primaryColorName); - } catch (e) { - debugPrint( - "Theme preset $primaryColorName not found. Applying default preset.", - ); - appSettingsProvider.setSetting( - AppSettingsEnum.primaryColor, - defaultColorPresetName, - ); - return defaultColorPreset; - } -}); - -final dynamicThemeSettingProvider = StateProvider((ref) { - return ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.dynamicTheme); -}); - -final colorfulInterfaceSettingProvider = StateProvider((ref) { - return ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.colorfulInterface); -}); - -// Provider for current selected theme -final immichThemeProvider = StateProvider((ref) { - var primaryColor = ref.read(immichThemePresetProvider); - var useSystemColor = ref.watch(dynamicThemeSettingProvider); - var useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); - - var currentTheme = (useSystemColor && _immichDynamicTheme != null) - ? _immichDynamicTheme! - : primaryColor.getTheme(); - - return useColorfulInterface - ? currentTheme - : _decolorizeSurfaces(theme: currentTheme); -}); - -// Method to fetch dynamic system colors -Future fetchSystemPalette() async { - try { - final corePalette = await DynamicColorPlugin.getCorePalette(); - if (corePalette != null) { - final primaryColor = corePalette.toColorScheme().primary; - debugPrint('dynamic_color: Core palette detected.'); - - // Some palettes do not generate surface container colors accurately, - // so we regenerate all colors using the primary color - _immichDynamicTheme = ImmichTheme( - light: ColorScheme.fromSeed( - seedColor: primaryColor, - brightness: Brightness.light, + return ThemeData( + useMaterial3: true, + brightness: colorScheme.brightness, + colorScheme: colorScheme, + primaryColor: colorScheme.primary, + hintColor: colorScheme.onSurfaceSecondary, + focusColor: colorScheme.primary, + scaffoldBackgroundColor: colorScheme.surface, + splashColor: colorScheme.primary.withOpacity(0.1), + highlightColor: colorScheme.primary.withOpacity(0.1), + dialogBackgroundColor: colorScheme.surfaceContainer, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: colorScheme.surfaceContainer, + ), + fontFamily: _getFontFamilyFromLocale(locale), + snackBarTheme: SnackBarThemeData( + contentTextStyle: TextStyle( + fontFamily: _getFontFamilyFromLocale(locale), + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), + backgroundColor: colorScheme.surfaceContainerHighest, + ), + appBarTheme: AppBarTheme( + titleTextStyle: TextStyle( + color: colorScheme.primary, + fontFamily: _getFontFamilyFromLocale(locale), + fontWeight: FontWeight.bold, + fontSize: 18, + ), + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + foregroundColor: colorScheme.primary, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + ), + textTheme: const TextTheme( + displayLarge: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + ), + displayMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + displaySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + titleSmall: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + titleMedium: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + titleLarge: TextStyle( + fontSize: 26.0, + fontWeight: FontWeight.bold, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: isDark ? Colors.black87 : Colors.white, + ), + ), + chipTheme: const ChipThemeData( + side: BorderSide.none, + ), + sliderTheme: const SliderThemeData( + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), + trackHeight: 2.0, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + ), + popupMenuTheme: const PopupMenuThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + labelTextStyle: const WidgetStatePropertyAll( + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, ), - dark: ColorScheme.fromSeed( - seedColor: primaryColor, - brightness: Brightness.dark, + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.primary, ), - ); - } - } catch (e) { - debugPrint('dynamic_color: Failed to obtain core palette.'); - } + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + labelStyle: TextStyle( + color: colorScheme.primary, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: colorScheme.primary, + ), + dropdownMenuTheme: DropdownMenuThemeData( + menuStyle: const MenuStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.primary, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + labelStyle: TextStyle( + color: colorScheme.primary, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + ), + ); } // This method replaces all surface shades in ImmichTheme to a static ones // as we are creating the colorscheme through seedColor the default surfaces are // tinted with primary color -ImmichTheme _decolorizeSurfaces({ +ImmichTheme decolorizeSurfaces({ required ImmichTheme theme, }) { return ImmichTheme( @@ -146,167 +204,10 @@ ImmichTheme _decolorizeSurfaces({ ); } -String? getFontFamilyFromLocale(Locale locale) { +String? _getFontFamilyFromLocale(Locale locale) { if (localesNotSupportedByOverpass.contains(locale)) { // Let Flutter use the default font return null; } return 'Overpass'; } - -ThemeData getThemeData({ - required ColorScheme colorScheme, - required Locale locale, -}) { - var isDark = colorScheme.brightness == Brightness.dark; - var primaryColor = colorScheme.primary; - - return ThemeData( - useMaterial3: true, - brightness: colorScheme.brightness, - colorScheme: colorScheme, - primaryColor: primaryColor, - hintColor: colorScheme.onSurfaceSecondary, - focusColor: primaryColor, - scaffoldBackgroundColor: colorScheme.surface, - splashColor: primaryColor.withOpacity(0.1), - highlightColor: primaryColor.withOpacity(0.1), - dialogBackgroundColor: colorScheme.surfaceContainer, - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: colorScheme.surfaceContainer, - ), - fontFamily: getFontFamilyFromLocale(locale), - snackBarTheme: SnackBarThemeData( - contentTextStyle: TextStyle( - fontFamily: getFontFamilyFromLocale(locale), - color: primaryColor, - fontWeight: FontWeight.bold, - ), - backgroundColor: colorScheme.surfaceContainerHighest, - ), - appBarTheme: AppBarTheme( - titleTextStyle: TextStyle( - color: primaryColor, - fontFamily: getFontFamilyFromLocale(locale), - fontWeight: FontWeight.bold, - fontSize: 18, - ), - backgroundColor: - isDark ? colorScheme.surfaceContainer : colorScheme.surface, - foregroundColor: primaryColor, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - titleSmall: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - titleLarge: TextStyle( - fontSize: 26.0, - fontWeight: FontWeight.bold, - ), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, - foregroundColor: isDark ? Colors.black87 : Colors.white, - ), - ), - chipTheme: const ChipThemeData( - side: BorderSide.none, - ), - sliderTheme: const SliderThemeData( - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), - trackHeight: 2.0, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - ), - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - ), - navigationBarTheme: NavigationBarThemeData( - backgroundColor: - isDark ? colorScheme.surfaceContainer : colorScheme.surface, - labelTextStyle: const WidgetStatePropertyAll( - TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: primaryColor, - ), - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - labelStyle: TextStyle( - color: primaryColor, - ), - hintStyle: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - ), - ), - textSelectionTheme: TextSelectionThemeData( - cursorColor: primaryColor, - ), - dropdownMenuTheme: DropdownMenuThemeData( - menuStyle: MenuStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: primaryColor, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - labelStyle: TextStyle( - color: primaryColor, - ), - hintStyle: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - ), - ), - ), - ); -} diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart index e4dd355554..f5479ab86e 100644 --- a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart +++ b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; class MotionPhotoButton extends ConsumerWidget { diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart index b1f70b8686..4d0e7aa17f 100644 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ b/mobile/lib/widgets/asset_viewer/video_position.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/formatted_duration.dart'; diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart index 4bbc040d4d..4df3e50f64 100644 --- a/mobile/lib/widgets/backup/error_chip.dart +++ b/mobile/lib/widgets/backup/error_chip.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart index 94148da176..540e136722 100644 --- a/mobile/lib/widgets/backup/error_chip_text.dart +++ b/mobile/lib/widgets/backup/error_chip_text.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; class BackupErrorChipText extends ConsumerWidget { diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index 68a2146bfb..65425f9e78 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -3,21 +3,22 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; /// Overrides the theme below the widget tree to use the theme data based on the /// map settings instead of the one from the app settings -class MapThemeOveride extends StatefulHookConsumerWidget { +class MapThemeOverride extends StatefulHookConsumerWidget { final ThemeMode? themeMode; final Widget Function(AsyncValue style) mapBuilder; - const MapThemeOveride({required this.mapBuilder, this.themeMode, super.key}); + const MapThemeOverride({required this.mapBuilder, this.themeMode, super.key}); @override - ConsumerState createState() => _MapThemeOverideState(); + ConsumerState createState() => _MapThemeOverrideState(); } -class _MapThemeOverideState extends ConsumerState +class _MapThemeOverrideState extends ConsumerState with WidgetsBindingObserver { late ThemeMode _theme; bool _isDarkTheme = false; diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index d02c016791..b856f09787 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -62,7 +62,7 @@ class MapThumbnail extends HookConsumerWidget { } } - return MapThemeOveride( + return MapThemeOverride( themeMode: themeMode, mapBuilder: (style) => SizedBox( height: height, diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 1c7cd1f207..119407ccad 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -2,12 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/theme/color_scheme.dart'; +import 'package:immich_mobile/theme/dynamic_theme.dart'; class PrimaryColorSetting extends HookConsumerWidget { const PrimaryColorSetting({ @@ -124,7 +126,7 @@ class PrimaryColorSetting extends HookConsumerWidget { style: context.textTheme.titleLarge, ), ), - if (isDynamicThemeAvailable) + if (DynamicTheme.isAvailable) Container( padding: const EdgeInsets.symmetric(horizontal: 20), margin: const EdgeInsets.only(top: 10), @@ -153,16 +155,16 @@ class PrimaryColorSetting extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 20), child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, - children: ImmichColorPreset.values.map((themePreset) { - var theme = themePreset.getTheme(); + children: ImmichColorPreset.values.map((preset) { + final theme = preset.themeOfPreset; return GestureDetector( - onTap: () => onPrimaryColorChange(themePreset), + onTap: () => onPrimaryColorChange(preset), child: buildPrimaryColorTile( topColor: theme.light.primary, bottomColor: theme.dark.primary, tileSize: tileSize, - showSelector: currentPreset.value == themePreset && + showSelector: currentPreset.value == preset && !systemPrimaryColorSetting.value, ), ); diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 3e1f388e84..b9ba7aa7b7 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; class ThemeSetting extends HookConsumerWidget { const ThemeSetting({ diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index c21f9bf166..bd000c8715 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -35,7 +35,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -53,7 +53,7 @@ void main() { testWidgets("Return error when style is not fetched", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -73,7 +73,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -94,7 +94,7 @@ void main() { testWidgets("Return dark theme style when system is dark", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -118,7 +118,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -142,7 +142,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock");