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} | 231 +++++------------- .../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, 255 insertions(+), 234 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,186 +10,45 @@ class ImmichTheme { const ImmichTheme({required this.light, required this.dark}); } -ImmichTheme? _immichDynamicTheme; -bool get isDynamicThemeAvailable => _immichDynamicTheme != null; - -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, - ), - dark: ColorScheme.fromSeed( - seedColor: primaryColor, - brightness: Brightness.dark, - ), - ); - } - } catch (e) { - debugPrint('dynamic_color: Failed to obtain core palette.'); - } -} - -// 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({ - required ImmichTheme theme, -}) { - return ImmichTheme( - light: theme.light.copyWith( - surface: const Color(0xFFf9f9f9), - onSurface: const Color(0xFF1b1b1b), - surfaceContainerLowest: const Color(0xFFffffff), - surfaceContainerLow: const Color(0xFFf3f3f3), - surfaceContainer: const Color(0xFFeeeeee), - surfaceContainerHigh: const Color(0xFFe8e8e8), - surfaceContainerHighest: const Color(0xFFe2e2e2), - surfaceDim: const Color(0xFFdadada), - surfaceBright: const Color(0xFFf9f9f9), - onSurfaceVariant: const Color(0xFF4c4546), - inverseSurface: const Color(0xFF303030), - onInverseSurface: const Color(0xFFf1f1f1), - ), - dark: theme.dark.copyWith( - surface: const Color(0xFF131313), - onSurface: const Color(0xFFE2E2E2), - surfaceContainerLowest: const Color(0xFF0E0E0E), - surfaceContainerLow: const Color(0xFF1B1B1B), - surfaceContainer: const Color(0xFF1F1F1F), - surfaceContainerHigh: const Color(0xFF242424), - surfaceContainerHighest: const Color(0xFF2E2E2E), - surfaceDim: const Color(0xFF131313), - surfaceBright: const Color(0xFF353535), - onSurfaceVariant: const Color(0xFFCfC4C5), - inverseSurface: const Color(0xFFE2E2E2), - onInverseSurface: const Color(0xFF303030), - ), - ); -} - -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; + final isDark = colorScheme.brightness == Brightness.dark; return ThemeData( useMaterial3: true, brightness: colorScheme.brightness, colorScheme: colorScheme, - primaryColor: primaryColor, + primaryColor: colorScheme.primary, hintColor: colorScheme.onSurfaceSecondary, - focusColor: primaryColor, + focusColor: colorScheme.primary, scaffoldBackgroundColor: colorScheme.surface, - splashColor: primaryColor.withOpacity(0.1), - highlightColor: primaryColor.withOpacity(0.1), + splashColor: colorScheme.primary.withOpacity(0.1), + highlightColor: colorScheme.primary.withOpacity(0.1), dialogBackgroundColor: colorScheme.surfaceContainer, bottomSheetTheme: BottomSheetThemeData( backgroundColor: colorScheme.surfaceContainer, ), - fontFamily: getFontFamilyFromLocale(locale), + fontFamily: _getFontFamilyFromLocale(locale), snackBarTheme: SnackBarThemeData( contentTextStyle: TextStyle( - fontFamily: getFontFamilyFromLocale(locale), - color: primaryColor, + fontFamily: _getFontFamilyFromLocale(locale), + color: colorScheme.primary, fontWeight: FontWeight.bold, ), backgroundColor: colorScheme.surfaceContainerHighest, ), appBarTheme: AppBarTheme( titleTextStyle: TextStyle( - color: primaryColor, - fontFamily: getFontFamilyFromLocale(locale), + color: colorScheme.primary, + fontFamily: _getFontFamilyFromLocale(locale), fontWeight: FontWeight.bold, fontSize: 18, ), backgroundColor: isDark ? colorScheme.surfaceContainer : colorScheme.surface, - foregroundColor: primaryColor, + foregroundColor: colorScheme.primary, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, @@ -226,7 +81,7 @@ ThemeData getThemeData({ ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, + backgroundColor: colorScheme.primary, foregroundColor: isDark ? Colors.black87 : Colors.white, ), ), @@ -258,7 +113,7 @@ ThemeData getThemeData({ inputDecorationTheme: InputDecorationTheme( focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: primaryColor, + color: colorScheme.primary, ), borderRadius: const BorderRadius.all(Radius.circular(15)), ), @@ -269,7 +124,7 @@ ThemeData getThemeData({ borderRadius: const BorderRadius.all(Radius.circular(15)), ), labelStyle: TextStyle( - color: primaryColor, + color: colorScheme.primary, ), hintStyle: const TextStyle( fontSize: 14.0, @@ -277,20 +132,20 @@ ThemeData getThemeData({ ), ), textSelectionTheme: TextSelectionThemeData( - cursorColor: primaryColor, + cursorColor: colorScheme.primary, ), dropdownMenuTheme: DropdownMenuThemeData( - menuStyle: MenuStyle( + menuStyle: const MenuStyle( shape: WidgetStatePropertyAll( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), + borderRadius: BorderRadius.all(Radius.circular(15)), ), ), ), inputDecorationTheme: InputDecorationTheme( focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: primaryColor, + color: colorScheme.primary, ), ), enabledBorder: OutlineInputBorder( @@ -300,7 +155,7 @@ ThemeData getThemeData({ borderRadius: const BorderRadius.all(Radius.circular(15)), ), labelStyle: TextStyle( - color: primaryColor, + color: colorScheme.primary, ), hintStyle: const TextStyle( fontSize: 14.0, @@ -310,3 +165,49 @@ ThemeData getThemeData({ ), ); } + +// 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({ + required ImmichTheme theme, +}) { + return ImmichTheme( + light: theme.light.copyWith( + surface: const Color(0xFFf9f9f9), + onSurface: const Color(0xFF1b1b1b), + surfaceContainerLowest: const Color(0xFFffffff), + surfaceContainerLow: const Color(0xFFf3f3f3), + surfaceContainer: const Color(0xFFeeeeee), + surfaceContainerHigh: const Color(0xFFe8e8e8), + surfaceContainerHighest: const Color(0xFFe2e2e2), + surfaceDim: const Color(0xFFdadada), + surfaceBright: const Color(0xFFf9f9f9), + onSurfaceVariant: const Color(0xFF4c4546), + inverseSurface: const Color(0xFF303030), + onInverseSurface: const Color(0xFFf1f1f1), + ), + dark: theme.dark.copyWith( + surface: const Color(0xFF131313), + onSurface: const Color(0xFFE2E2E2), + surfaceContainerLowest: const Color(0xFF0E0E0E), + surfaceContainerLow: const Color(0xFF1B1B1B), + surfaceContainer: const Color(0xFF1F1F1F), + surfaceContainerHigh: const Color(0xFF242424), + surfaceContainerHighest: const Color(0xFF2E2E2E), + surfaceDim: const Color(0xFF131313), + surfaceBright: const Color(0xFF353535), + onSurfaceVariant: const Color(0xFFCfC4C5), + inverseSurface: const Color(0xFFE2E2E2), + onInverseSurface: const Color(0xFF303030), + ), + ); +} + +String? _getFontFamilyFromLocale(Locale locale) { + if (localesNotSupportedByOverpass.contains(locale)) { + // Let Flutter use the default font + return null; + } + return 'Overpass'; +} 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");