diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index ad3103b002..220a73b58d 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -531,6 +531,11 @@ "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", @@ -562,4 +567,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} \ No newline at end of file +} diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index 598f956619..38deac3f0e 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -1,5 +1,108 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/utils/immich_app_theme.dart'; -const Color immichBackgroundColor = Color(0xFFf6f8fe); -const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0); -const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250); +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); + +final Map _themePresetsMap = { + ImmichColorPreset.indigo: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: immichBrandColorLight, + ).copyWith(primary: immichBrandColorLight), + dark: ColorScheme.fromSeed( + seedColor: immichBrandColorDark, + brightness: Brightness.dark, + ).copyWith(primary: immichBrandColorDark), + ), + ImmichColorPreset.deepPurple: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF6F43C0)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFD3BBFF), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.pink: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFED79B5)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFED79B5), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.red: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFC51C16)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFD3302F), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.orange: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: const Color(0xffff5b01), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFCC6D08), + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), + ), + ImmichColorPreset.yellow: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFFFB400)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFFFB400), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.lime: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFCDDC39)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFCDDC39), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.green: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF18C249)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFF18C249), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.cyan: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF00BCD4)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFF00BCD4), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.slateGray: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: const Color(0xFF696969), + dynamicSchemeVariant: DynamicSchemeVariant.neutral, + ), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xff696969), + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.neutral, + ), + ), +}; + +extension ImmichColorModeExtension on ImmichColorPreset { + ImmichTheme getTheme() => _themePresetsMap[this]!; +} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index baa7ff51a3..a84f980001 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -229,6 +229,11 @@ enum StoreKey { mapwithPartners(125, type: bool), enableHapticFeedback(126, type: bool), customHeaders(127, type: String), + + // theme settings + primaryColor(128, type: String), + dynamicTheme(129, type: bool), + colorfulInterface(130, type: bool), ; const StoreKey( diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart index 6a61b00530..141a1ede15 100644 --- a/mobile/lib/extensions/build_context_extensions.dart +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -20,10 +20,10 @@ extension ContextHelper on BuildContext { bool get isDarkTheme => themeData.brightness == Brightness.dark; // Returns the current Primary color of the Theme - Color get primaryColor => themeData.primaryColor; + Color get primaryColor => themeData.colorScheme.primary; // Returns the Scaffold background color of the Theme - Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor; + Color get scaffoldBackgroundColor => colorScheme.surface; // Returns the current TextTheme TextTheme get textTheme => themeData.textTheme; diff --git a/mobile/lib/extensions/theme_extensions.dart b/mobile/lib/extensions/theme_extensions.dart new file mode 100644 index 0000000000..3e17e2b991 --- /dev/null +++ b/mobile/lib/extensions/theme_extensions.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +extension ImmichColorSchemeExtensions on ColorScheme { + bool get _isDarkMode => brightness == Brightness.dark; + Color get onSurfaceSecondary => _isDarkMode + ? onSurface.darken(amount: .3) + : onSurface.lighten(amount: .3); +} + +extension ColorExtensions on Color { + Color lighten({double amount = 0.1}) { + return Color.alphaBlend( + Colors.white.withOpacity(amount), + this, + ); + } + + Color darken({double amount = 0.1}) { + return Color.alphaBlend( + Colors.black.withOpacity(amount), + this, + ); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 2340ed70d2..916c1ad3d3 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -65,6 +65,8 @@ Future initApp() async { } } + await fetchSystemPalette(); + // Initialize Immich Logger Service ImmichLogger(); @@ -187,6 +189,7 @@ class ImmichAppState extends ConsumerState @override Widget build(BuildContext context) { var router = ref.watch(appRouterProvider); + var immichTheme = ref.watch(immichThemeProvider); return MaterialApp( localizationsDelegates: context.localizationDelegates, @@ -196,9 +199,9 @@ class ImmichAppState extends ConsumerState home: MaterialApp.router( title: 'Immich', debugShowCheckedModeBanner: false, - themeMode: ref.watch(immichThemeProvider), - darkTheme: immichDarkTheme, - theme: immichLightTheme, + themeMode: ref.watch(immichThemeModeProvider), + darkTheme: getThemeData(colorScheme: immichTheme.dark), + theme: getThemeData(colorScheme: immichTheme.light), routeInformationParser: router.defaultRouteParser(), routerDelegate: router.delegate( navigatorObservers: () => [TabNavigationObserver(ref: ref)], diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart index 218127ff43..5cb5d418a0 100644 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ b/mobile/lib/pages/backup/album_preview.page.dart @@ -4,6 +4,8 @@ import 'package:auto_route/auto_route.dart'; 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -46,7 +48,7 @@ class AlbumPreviewPage extends HookConsumerWidget { "ID ${album.id}", style: TextStyle( fontSize: 10, - color: Colors.grey[600], + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index ecfebd3cb7..9f3e387755 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -3,7 +3,6 @@ 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/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/widgets/backup/album_info_card.dart'; @@ -128,13 +127,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { album.name, style: TextStyle( fontSize: 12, - color: isDarkTheme ? Colors.black : immichBackgroundColor, + color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold, ), ), backgroundColor: Colors.red[300], - deleteIconColor: - isDarkTheme ? Colors.black : immichBackgroundColor, + deleteIconColor: context.scaffoldBackgroundColor, deleteIcon: const Icon( Icons.cancel_rounded, size: 15, diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 89384cf97a..61a6bc1bb9 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -7,6 +7,7 @@ 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -130,9 +131,7 @@ class BackupControllerPage extends HookConsumerWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 56, 56, 56) - : Colors.black12, + color: context.colorScheme.outlineVariant, width: 1, ), ), @@ -151,7 +150,9 @@ class BackupControllerPage extends HookConsumerWidget { children: [ Text( "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ).tr(), buildSelectedAlbumName(), buildExcludedAlbumName(), diff --git a/mobile/lib/pages/common/album_options.page.dart b/mobile/lib/pages/common/album_options.page.dart index 1cc24af09c..3cc30af7a9 100644 --- a/mobile/lib/pages/common/album_options.page.dart +++ b/mobile/lib/pages/common/album_options.page.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; @@ -102,7 +103,7 @@ class AlbumOptionsPage extends HookConsumerWidget { } showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, + backgroundColor: context.colorScheme.surfaceContainer, isScrollControlled: false, context: context, builder: (context) { @@ -131,7 +132,7 @@ class AlbumOptionsPage extends HookConsumerWidget { ), subtitle: Text( album.owner.value?.email ?? "", - style: TextStyle(color: Colors.grey[600]), + style: TextStyle(color: context.colorScheme.onSurfaceSecondary), ), trailing: Text( "shared_album_section_people_owner_label", @@ -160,7 +161,9 @@ class AlbumOptionsPage extends HookConsumerWidget { ), subtitle: Text( user.email, - style: TextStyle(color: Colors.grey[600]), + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, + ), ), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) @@ -214,7 +217,7 @@ class AlbumOptionsPage extends HookConsumerWidget { subtitle: Text( "shared_album_activity_setting_subtitle", style: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(175), + color: context.colorScheme.onSurfaceSecondary, ), ).tr(), ), diff --git a/mobile/lib/pages/common/album_viewer.page.dart b/mobile/lib/pages/common/album_viewer.page.dart index e1e0419d52..33b314f3b1 100644 --- a/mobile/lib/pages/common/album_viewer.page.dart +++ b/mobile/lib/pages/common/album_viewer.page.dart @@ -14,7 +14,7 @@ import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; @@ -114,13 +114,13 @@ class AlbumViewerPage extends HookConsumerWidget { child: ListView( scrollDirection: Axis.horizontal, children: [ - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: () => onAddPhotosPressed(album), labelText: "share_add_photos".tr(), ), if (userId == album.ownerId) - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.person_add_alt_rounded, onPressed: () => onAddUsersPressed(album), labelText: "album_viewer_page_share_add_users".tr(), diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 8066835d84..fd718ee37d 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -3,6 +3,7 @@ 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; @@ -18,7 +19,6 @@ class AppLogPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final immichLogger = ImmichLogger(); final logMessages = useState(immichLogger.messages); - final isDarkTheme = context.isDarkTheme; Widget colorStatusIndicator(Color color) { return Column( @@ -55,13 +55,9 @@ class AppLogPage extends HookConsumerWidget { case LogLevel.INFO: return Colors.transparent; case LogLevel.SEVERE: - return isDarkTheme - ? Colors.redAccent.withOpacity(0.25) - : Colors.redAccent.withOpacity(0.075); + return Colors.redAccent.withOpacity(0.25); case LogLevel.WARNING: - return isDarkTheme - ? Colors.orangeAccent.withOpacity(0.25) - : Colors.orangeAccent.withOpacity(0.075); + return Colors.orangeAccent.withOpacity(0.25); default: return context.primaryColor.withOpacity(0.1); } @@ -120,10 +116,7 @@ class AppLogPage extends HookConsumerWidget { ), body: ListView.separated( separatorBuilder: (context, index) { - return Divider( - height: 0, - color: isDarkTheme ? Colors.white70 : Colors.grey[600], - ); + return const Divider(height: 0); }, itemCount: logMessages.value.length, itemBuilder: (context, index) { @@ -141,8 +134,9 @@ class AppLogPage extends HookConsumerWidget { minLeadingWidth: 10, title: Text( truncateLogMessage(logMessage.message, 4), - style: const TextStyle( + style: TextStyle( fontSize: 14.0, + color: context.colorScheme.onSurface, fontFamily: "Inconsolata", ), ), @@ -150,7 +144,7 @@ class AppLogPage extends HookConsumerWidget { "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}", style: TextStyle( fontSize: 12.0, - color: Colors.grey[600], + color: context.colorScheme.onSurfaceSecondary, ), ), leading: buildLeadingIcon(logMessage.level), diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 61f510c0de..1b9af6cfcf 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -13,8 +13,6 @@ class AppLogDetailPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isDarkTheme = context.isDarkTheme; - buildTextWithCopyButton(String header, String text) { return Padding( padding: const EdgeInsets.all(8.0), @@ -61,7 +59,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), Container( decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(15.0), ), child: Padding( @@ -100,7 +98,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), Container( decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(15.0), ), child: Padding( diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index 053057425e..1ed6885a07 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_title.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; @@ -109,20 +109,16 @@ class CreateAlbumPage extends HookConsumerWidget { if (selectedAssets.value.isEmpty) { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.only(top: 16, left: 18, right: 18), - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: FilledButton.icon( + style: FilledButton.styleFrom( alignment: Alignment.centerLeft, padding: - const EdgeInsets.symmetric(vertical: 22, horizontal: 16), - side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 63, 63, 63) - : const Color.fromARGB(255, 129, 129, 129), - ), + const EdgeInsets.symmetric(vertical: 16, horizontal: 16), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(10), ), + backgroundColor: context.colorScheme.surfaceContainerHighest, ), onPressed: onSelectPhotosButtonPressed, icon: Icon( @@ -134,7 +130,7 @@ class CreateAlbumPage extends HookConsumerWidget { child: Text( 'create_shared_album_page_share_select_photos', style: context.textTheme.titleMedium?.copyWith( - color: context.primaryColor, + fontWeight: FontWeight.normal, ), ).tr(), ), @@ -154,7 +150,7 @@ class CreateAlbumPage extends HookConsumerWidget { child: ListView( scrollDirection: Axis.horizontal, children: [ - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: onSelectPhotosButtonPressed, labelText: "share_add_photos".tr(), diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 486eeba4cd..117b0aedc0 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -49,10 +49,6 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( centerTitle: false, - bottom: const PreferredSize( - preferredSize: Size.fromHeight(1), - child: Divider(height: 1), - ), title: const Text('setting_pages_app_bar_settings').tr(), ), body: context.isMobile ? _MobileLayout() : _TabletLayout(), @@ -67,13 +63,18 @@ class _MobileLayout extends StatelessWidget { children: SettingSection.values .map( (s) => ListTile( - title: Text( - s.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ).tr(), + contentPadding: + const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0), leading: Icon(s.icon), + title: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + s.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + ), onTap: () => context.pushRoute(SettingsSubRoute(section: s)), ), ) @@ -102,7 +103,7 @@ class _TabletLayout extends HookWidget { leading: Icon(s.icon), selected: s.index == selectedSection.value.index, selectedColor: context.primaryColor, - selectedTileColor: context.primaryColor.withAlpha(50), + selectedTileColor: context.themeData.highlightColor, onTap: () => selectedSection.value = s, ), ), diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index be98440349..5f03ed6871 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -20,7 +20,6 @@ class LibraryPage extends HookConsumerWidget { final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); final albums = ref.watch(albumProvider); - final isDarkTheme = context.isDarkTheme; final albumSortOption = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); @@ -116,12 +115,7 @@ class LibraryPage extends HookConsumerWidget { width: cardSize, height: cardSize, decoration: BoxDecoration( - border: Border.all( - color: isDarkTheme - ? const Color.fromARGB(255, 53, 53, 53) - : const Color.fromARGB(255, 203, 203, 203), - ), - color: isDarkTheme ? Colors.grey[900] : Colors.grey[50], + color: context.colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(20)), ), child: Center( @@ -139,7 +133,9 @@ class LibraryPage extends HookConsumerWidget { ), child: Text( 'library_page_new_album', - style: context.textTheme.labelLarge, + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurface, + ), ).tr(), ), ], @@ -156,26 +152,25 @@ class LibraryPage extends HookConsumerWidget { Function() onClick, ) { return Expanded( - child: OutlinedButton.icon( + child: FilledButton.icon( onPressed: onClick, label: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( label, style: TextStyle( - color: context.isDarkTheme - ? Colors.white - : Colors.black.withAlpha(200), + color: context.colorScheme.onSurface, ), ), ), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50], - side: BorderSide( - color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!, - ), + style: FilledButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + backgroundColor: context.colorScheme.surfaceContainer, alignment: Alignment.centerLeft, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), ), icon: Icon( icon, @@ -247,6 +242,7 @@ class LibraryPage extends HookConsumerWidget { Text( 'library_page_albums', style: context.textTheme.bodyLarge?.copyWith( + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ).tr(), diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index 212145ed5a..b305b5fc53 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -3,6 +3,7 @@ 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/forms/login/login_form.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -39,8 +40,8 @@ class LoginPage extends HookConsumerWidget { children: [ Text( 'v${appVersion.value}', - style: const TextStyle( - color: Colors.grey, + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontFamily: "Inconsolata", ), diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 2c578925c1..173115185b 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/search/search_curated_content.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; @@ -38,7 +39,7 @@ class SearchPage extends HookConsumerWidget { fontSize: 15.0, ); - Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; + Color categoryIconColor = context.colorScheme.onSurface; showNameEditModel( String personId, @@ -128,13 +129,9 @@ class SearchPage extends HookConsumerWidget { }, child: Card( elevation: 0, + color: context.colorScheme.surfaceContainerHigh, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide( - color: context.isDarkTheme - ? Colors.grey[800]! - : const Color.fromARGB(255, 225, 225, 225), - ), + borderRadius: BorderRadius.circular(50), ), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( @@ -144,13 +141,15 @@ class SearchPage extends HookConsumerWidget { ), child: Row( children: [ - Icon(Icons.search, color: context.primaryColor), + Icon( + Icons.search, + color: context.colorScheme.onSurfaceSecondary, + ), const SizedBox(width: 16.0), Text( "search_bar_hint", style: context.textTheme.bodyLarge?.copyWith( - color: - context.isDarkTheme ? Colors.white70 : Colors.black54, + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.w400, ), ).tr(), diff --git a/mobile/lib/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart index 1f90f2929c..acabc75aa4 100644 --- a/mobile/lib/pages/search/search_input.page.dart +++ b/mobile/lib/pages/search/search_input.page.dart @@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; @@ -509,7 +510,7 @@ class SearchInputPage extends HookConsumerWidget { ? 'contextual_search'.tr() : 'filename_search'.tr(), hintStyle: context.textTheme.bodyLarge?.copyWith( - color: context.themeData.colorScheme.onSurface.withOpacity(0.75), + color: context.themeData.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.w500, ), enabledBorder: const UnderlineInputBorder( diff --git a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart index 6223e110e1..5ed85932f8 100644 --- a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart @@ -30,6 +30,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { const padding = 20.0; final themeData = context.themeData; + final colorScheme = context.colorScheme; final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); @@ -58,7 +59,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Text( existingLink!.title, style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), @@ -81,7 +82,7 @@ class SharedLinkEditPage extends HookConsumerWidget { child: Text( existingLink!.description ?? "--", style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, @@ -109,7 +110,7 @@ class SharedLinkEditPage extends HookConsumerWidget { labelText: 'shared_link_edit_description'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), floatingLabelBehavior: FloatingLabelBehavior.always, border: const OutlineInputBorder(), @@ -135,7 +136,7 @@ class SharedLinkEditPage extends HookConsumerWidget { labelText: 'shared_link_edit_password'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), floatingLabelBehavior: FloatingLabelBehavior.always, border: const OutlineInputBorder(), @@ -157,7 +158,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_show_meta", @@ -173,7 +174,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_allow_download", @@ -189,7 +190,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_allow_upload", @@ -205,7 +206,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_change_expiry", @@ -221,7 +222,7 @@ class SharedLinkEditPage extends HookConsumerWidget { "shared_link_edit_expire_after", style: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), ).tr(), enableSearch: false, diff --git a/mobile/lib/pages/sharing/sharing.page.dart b/mobile/lib/pages/sharing/sharing.page.dart index 45148945ed..98d4cfafe9 100644 --- a/mobile/lib/pages/sharing/sharing.page.dart +++ b/mobile/lib/pages/sharing/sharing.page.dart @@ -4,6 +4,7 @@ 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; @@ -83,20 +84,24 @@ class SharingPage extends HookConsumerWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), subtitle: isOwner ? Text( 'album_thumbnail_owned'.tr(), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ) : album.ownerName != null ? Text( 'album_thumbnail_shared_by' .tr(args: [album.ownerName!]), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ) : null, onTap: () => context @@ -166,11 +171,13 @@ class SharingPage extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Card( elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), side: BorderSide( - color: Colors.grey, - width: 0.5, + color: context.isDarkTheme + ? const Color(0xFF383838) + : Colors.black12, + width: 1, ), ), child: Padding( diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index fd6c2d89a7..bd25403215 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { @@ -8,6 +9,21 @@ enum AppSettingsEnum { "themeMode", "system", ), // "light","dark","system" + primaryColor( + StoreKey.primaryColor, + "primaryColor", + defaultColorPresetName, + ), + dynamicTheme( + StoreKey.dynamicTheme, + "dynamicTheme", + false, + ), + colorfulInterface( + StoreKey.colorfulInterface, + "colorfulInterface", + true, + ), tilesPerRow(StoreKey.tilesPerRow, "tilesPerRow", 4), dynamicLayout(StoreKey.dynamicLayout, "dynamicLayout", false), groupAssetsBy(StoreKey.groupAssetsBy, "groupBy", 0), diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index 32a26439d5..d61eba73b2 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -1,10 +1,22 @@ +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/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -final immichThemeProvider = StateProvider((ref) { +class ImmichTheme { + ColorScheme light; + ColorScheme dark; + + 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); @@ -20,266 +32,241 @@ final immichThemeProvider = StateProvider((ref) { } }); -final ThemeData base = ThemeData( - chipTheme: const ChipThemeData( - side: BorderSide.none, - ), - sliderTheme: const SliderThemeData( - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), - trackHeight: 2.0, - ), -); +final immichThemePresetProvider = StateProvider((ref) { + var appSettingsProvider = ref.watch(appSettingsServiceProvider); + var primaryColorName = + appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); -final ThemeData immichLightTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.light, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.indigo, - ), - primarySwatch: Colors.indigo, - primaryColor: Colors.indigo, - hintColor: Colors.indigo, - focusColor: Colors.indigo, - splashColor: Colors.indigo.withOpacity(0.15), - fontFamily: 'Overpass', - scaffoldBackgroundColor: immichBackgroundColor, - snackBarTheme: const SnackBarThemeData( - contentTextStyle: TextStyle( - fontFamily: 'Overpass', - color: Colors.indigo, - fontWeight: FontWeight.bold, - ), - backgroundColor: Colors.white, - ), - appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle( - fontFamily: 'Overpass', - color: Colors.indigo, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - backgroundColor: immichBackgroundColor, - foregroundColor: Colors.indigo, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - backgroundColor: immichBackgroundColor, - selectedItemColor: Colors.indigo, - ), - cardTheme: const CardTheme( - surfaceTintColor: Colors.transparent, - ), - drawerTheme: const DrawerThemeData( - backgroundColor: immichBackgroundColor, - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - 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: Colors.indigo, - foregroundColor: Colors.white, - ), - ), - chipTheme: base.chipTheme, - sliderTheme: base.sliderTheme, - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - surfaceTintColor: Colors.transparent, - color: Colors.white, - ), - navigationBarTheme: NavigationBarThemeData( - indicatorColor: Colors.indigo.withOpacity(0.15), - iconTheme: WidgetStatePropertyAll( - IconThemeData(color: Colors.grey[700]), - ), - backgroundColor: immichBackgroundColor, - surfaceTintColor: Colors.transparent, - labelTextStyle: WidgetStatePropertyAll( - TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey[800], - ), - ), - ), - dialogTheme: const DialogTheme( - surfaceTintColor: Colors.transparent, - ), - inputDecorationTheme: const InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.indigo, - ), - ), - labelStyle: TextStyle( - color: Colors.indigo, - ), - hintStyle: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - ), - ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Colors.indigo, - ), -); + debugPrint("Current theme preset $primaryColorName"); -final ThemeData immichDarkTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - primarySwatch: Colors.indigo, - primaryColor: immichDarkThemePrimaryColor, - colorScheme: ColorScheme.fromSeed( - seedColor: immichDarkThemePrimaryColor, - brightness: Brightness.dark, - ), - scaffoldBackgroundColor: immichDarkBackgroundColor, - hintColor: Colors.grey[600], - fontFamily: 'Overpass', - snackBarTheme: SnackBarThemeData( - contentTextStyle: const TextStyle( - fontFamily: 'Overpass', - color: immichDarkThemePrimaryColor, - fontWeight: FontWeight.bold, + 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), ), - backgroundColor: Colors.grey[900], - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: immichDarkThemePrimaryColor, + 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), ), - ), - appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle( - fontFamily: 'Overpass', - color: immichDarkThemePrimaryColor, - fontWeight: FontWeight.bold, - fontSize: 18, + ); +} + +ThemeData getThemeData({required ColorScheme colorScheme}) { + var isDark = colorScheme.brightness == Brightness.dark; + var primaryColor = colorScheme.primary; + + return ThemeData( + useMaterial3: true, + brightness: isDark ? Brightness.dark : Brightness.light, + 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, ), - backgroundColor: Color.fromARGB(255, 32, 33, 35), - foregroundColor: immichDarkThemePrimaryColor, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - backgroundColor: Color.fromARGB(255, 35, 36, 37), - selectedItemColor: immichDarkThemePrimaryColor, - ), - drawerTheme: DrawerThemeData( - backgroundColor: immichDarkBackgroundColor, - scrimColor: Colors.white.withOpacity(0.1), - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 255, 255, 255), + fontFamily: 'Overpass', + snackBarTheme: SnackBarThemeData( + contentTextStyle: TextStyle( + fontFamily: 'Overpass', + color: primaryColor, + fontWeight: FontWeight.bold, + ), + backgroundColor: colorScheme.surfaceContainerHighest, ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 255, 255, 255), + appBarTheme: AppBarTheme( + titleTextStyle: TextStyle( + color: primaryColor, + fontFamily: 'Overpass', + fontWeight: FontWeight.bold, + fontSize: 18, + ), + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + foregroundColor: primaryColor, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: immichDarkThemePrimaryColor, - ), - titleSmall: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - titleLarge: TextStyle( - fontSize: 26.0, - fontWeight: FontWeight.bold, - ), - ), - cardColor: Colors.grey[900], - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.black87, - backgroundColor: immichDarkThemePrimaryColor, - ), - ), - chipTheme: base.chipTheme, - sliderTheme: base.sliderTheme, - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - surfaceTintColor: Colors.transparent, - ), - navigationBarTheme: NavigationBarThemeData( - indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4), - iconTheme: WidgetStatePropertyAll( - IconThemeData(color: Colors.grey[500]), - ), - backgroundColor: Colors.grey[900], - surfaceTintColor: Colors.transparent, - labelTextStyle: WidgetStatePropertyAll( - TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey[300], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : primaryColor, + ), + displayMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, + ), + displaySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: primaryColor, + ), + titleSmall: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + titleMedium: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + titleLarge: const TextStyle( + fontSize: 26.0, + fontWeight: FontWeight.bold, ), ), - ), - dialogTheme: const DialogTheme( - surfaceTintColor: Colors.transparent, - ), - inputDecorationTheme: const InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: immichDarkThemePrimaryColor, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: isDark ? Colors.black87 : Colors.white, ), ), - labelStyle: TextStyle( - color: immichDarkThemePrimaryColor, + chipTheme: const ChipThemeData( + side: BorderSide.none, ), - hintStyle: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, + sliderTheme: const SliderThemeData( + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), + trackHeight: 2.0, ), - ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: immichDarkThemePrimaryColor, - ), -); + 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: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: primaryColor, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + ), + labelStyle: TextStyle( + color: primaryColor, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: primaryColor, + ), + ); +} diff --git a/mobile/lib/widgets/album/album_action_outlined_button.dart b/mobile/lib/widgets/album/album_action_filled_button.dart similarity index 70% rename from mobile/lib/widgets/album/album_action_outlined_button.dart rename to mobile/lib/widgets/album/album_action_filled_button.dart index 02676ae6e2..6a466aa4f1 100644 --- a/mobile/lib/widgets/album/album_action_outlined_button.dart +++ b/mobile/lib/widgets/album/album_action_filled_button.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -class AlbumActionOutlinedButton extends StatelessWidget { +class AlbumActionFilledButton extends StatelessWidget { final VoidCallback? onPressed; final String labelText; final IconData iconData; - const AlbumActionOutlinedButton({ + const AlbumActionFilledButton({ super.key, this.onPressed, required this.labelText, @@ -17,18 +17,13 @@ class AlbumActionOutlinedButton extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 16.0), - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( + child: FilledButton.icon( + style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), - side: BorderSide( - width: 1, - color: context.isDarkTheme - ? const Color.fromARGB(255, 63, 63, 63) - : const Color.fromARGB(255, 206, 206, 206), - ), + backgroundColor: context.colorScheme.surfaceContainerHigh, ), icon: Icon( iconData, diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 737e8b383f..42fa55cdd4 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class AlbumThumbnailCard extends StatelessWidget { @@ -23,8 +24,6 @@ class AlbumThumbnailCard extends StatelessWidget { @override Widget build(BuildContext context) { - var isDarkTheme = context.isDarkTheme; - return LayoutBuilder( builder: (context, constraints) { var cardSize = constraints.maxWidth; @@ -34,12 +33,13 @@ class AlbumThumbnailCard extends StatelessWidget { height: cardSize, width: cardSize, decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[800] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, ), child: Center( child: Icon( Icons.no_photography, size: cardSize * .15, + color: context.colorScheme.primary, ), ), ); @@ -65,6 +65,9 @@ class AlbumThumbnailCard extends StatelessWidget { return RichText( overflow: TextOverflow.fade, text: TextSpan( + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), children: [ TextSpan( text: album.assetCount == 1 @@ -72,14 +75,9 @@ class AlbumThumbnailCard extends StatelessWidget { .tr(args: ['${album.assetCount}']) : 'album_thumbnail_card_items' .tr(args: ['${album.assetCount}']), - style: context.textTheme.bodyMedium, ), if (owner != null) const TextSpan(text: ' ยท '), - if (owner != null) - TextSpan( - text: owner, - style: context.textTheme.bodyMedium, - ), + if (owner != null) TextSpan(text: owner), ], ), ); @@ -112,7 +110,7 @@ class AlbumThumbnailCard extends StatelessWidget { album.name, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart index 8715c0c038..d005a96417 100644 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ b/mobile/lib/widgets/album/album_title_text_field.dart @@ -20,8 +20,6 @@ class AlbumTitleTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isDarkTheme = context.isDarkTheme; - return TextField( onChanged: (v) { if (v.isEmpty) { @@ -35,7 +33,7 @@ class AlbumTitleTextField extends ConsumerWidget { focusNode: albumTitleTextFieldFocusNode, style: TextStyle( fontSize: 28, - color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], + color: context.colorScheme.onSurface, fontWeight: FontWeight.bold, ), controller: albumTitleController, @@ -61,24 +59,18 @@ class AlbumTitleTextField extends ConsumerWidget { splashRadius: 10, ) : null, - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), hintText: 'share_add_title'.tr(), - hintStyle: TextStyle( + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28, - color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.bold, ), focusColor: Colors.grey[300], - fillColor: isDarkTheme - ? const Color.fromARGB(255, 32, 33, 35) - : Colors.grey[200], + fillColor: context.scaffoldBackgroundColor, filled: isAlbumTitleTextFieldFocus.value, ), ); diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart index 6fb58f8082..1067d7241e 100644 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ b/mobile/lib/widgets/album/album_viewer_appbar.dart @@ -95,7 +95,7 @@ class AlbumViewerAppbar extends HookConsumerWidget 'action_common_confirm', style: TextStyle( fontWeight: FontWeight.bold, - color: !context.isDarkTheme ? Colors.red : Colors.red[300], + color: context.colorScheme.error, ), ).tr(), ), diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart index 788c61d8a4..59e09aa050 100644 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ b/mobile/lib/widgets/album/album_viewer_editable_title.dart @@ -73,24 +73,18 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { splashRadius: 10, ) : null, - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), focusColor: Colors.grey[300], - fillColor: context.isDarkTheme - ? const Color.fromARGB(255, 32, 33, 35) - : Colors.grey[200], + fillColor: context.scaffoldBackgroundColor, filled: titleFocusNode.hasFocus, hintText: 'share_add_title'.tr(), - hintStyle: TextStyle( + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28, - color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.bold, ), ), ), diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart index 060e0bc04e..e6d769a3d7 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -281,7 +281,7 @@ class ControlBottomAppBar extends HookConsumerWidget { ScrollController scrollController, ) { return Card( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], + color: context.colorScheme.surfaceContainerLow, surfaceTintColor: Colors.transparent, elevation: 18.0, shape: const RoundedRectangleBorder( diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart index 9d26745b16..50b38c2a4a 100644 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart @@ -22,12 +22,15 @@ class DisableMultiSelectButton extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ElevatedButton.icon( onPressed: () => onPressed(), - icon: const Icon(Icons.close_rounded), + icon: Icon( + Icons.close_rounded, + color: context.colorScheme.onPrimary, + ), label: Text( '$selectedItemCount', style: context.textTheme.titleMedium?.copyWith( height: 2.5, - color: context.isDarkTheme ? Colors.black : Colors.white, + color: context.colorScheme.onPrimary, ), ), ), diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart index 4c1f468343..3a411c09db 100644 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ b/mobile/lib/widgets/asset_grid/group_divider_title.dart @@ -2,6 +2,7 @@ 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -74,9 +75,9 @@ class GroupDividerTitle extends HookConsumerWidget { Icons.check_circle_rounded, color: context.primaryColor, ) - : const Icon( + : Icon( Icons.check_circle_outline_rounded, - color: Colors.grey, + color: context.colorScheme.onSurfaceSecondary, ), ), ], diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index 906d0e5969..ea65031a0c 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; @@ -266,7 +267,9 @@ class ImmichAssetGridViewState extends ConsumerState { scrollStateListener: dragScrolling, itemPositionsListener: _itemPositionsListener, controller: _itemScrollController, - backgroundColor: context.themeData.hintColor, + backgroundColor: context.isDarkTheme + ? context.colorScheme.primary.darken(amount: .5) + : context.colorScheme.primary, labelTextBuilder: _labelBuilder, padding: appBarOffset() ? const EdgeInsets.only(top: 60) diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index d9c9aa0566..2480f44278 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; import 'package:isar/isar.dart'; @@ -42,8 +43,8 @@ class ThumbnailImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final assetContainerColor = context.isDarkTheme - ? Colors.blueGrey - : context.themeData.primaryColorLight; + ? context.primaryColor.darken(amount: 0.6) + : context.primaryColor.lighten(amount: 0.8); // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id final isFromDto = asset.id == Isar.autoIncrement; @@ -192,8 +193,8 @@ class ThumbnailImage extends ConsumerWidget { bottom: 5, child: Icon( storageIcon(asset), - color: Colors.white, - size: 18, + color: Colors.white.withOpacity(.8), + size: 16, ), ), if (asset.isFavorite) diff --git a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart index d762704835..5b12426a50 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class ThumbnailPlaceholder extends StatelessWidget { final EdgeInsets margin; @@ -13,25 +14,20 @@ class ThumbnailPlaceholder extends StatelessWidget { this.height = 250, }); - static const _brightColors = [ - Color(0xFFF1F3F4), - Color(0xFFB4B6B8), - ]; - - static const _darkColors = [ - Color(0xFF3B3F42), - Color(0xFF2B2F32), - ]; - @override Widget build(BuildContext context) { + var gradientColors = [ + context.colorScheme.surfaceContainer, + context.colorScheme.surfaceContainer.darken(amount: .1), + ]; + return Container( width: width, height: height, margin: margin, decoration: BoxDecoration( gradient: LinearGradient( - colors: context.isDarkTheme ? _darkColors : _brightColors, + colors: gradientColors, begin: Alignment.topCenter, end: Alignment.bottomCenter, ), diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 7422e43335..1a91d1614b 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/asset_description.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -23,7 +24,6 @@ class DescriptionInput extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; final controller = useTextEditingController(); final focusNode = useFocusNode(); final isFocus = useState(false); @@ -71,7 +71,7 @@ class DescriptionInput extends HookConsumerWidget { }, icon: Icon( Icons.cancel_rounded, - color: Colors.grey[500], + color: context.colorScheme.onSurfaceSecondary, ), splashRadius: 10, ); @@ -100,9 +100,6 @@ class DescriptionInput extends HookConsumerWidget { decoration: InputDecoration( hintText: 'description_input_hint_text'.tr(), border: InputBorder.none, - hintStyle: context.textTheme.labelLarge?.copyWith( - color: textColor.withOpacity(0.5), - ), suffixIcon: suffixIcon, ), ); diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart index a0505e3d48..ae32c133c3 100644 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart +++ b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart @@ -22,7 +22,7 @@ class ExifBottomSheet extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final assetWithExif = ref.watch(assetDetailProvider(asset)); - var textColor = context.isDarkTheme ? Colors.white : Colors.black; + var textColor = context.colorScheme.onSurface; final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; // Format the date time with the timezone final (dt, timeZone) = diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 70fd5e3b89..2157a1aebb 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -178,6 +178,7 @@ class TopControlAppBar extends HookConsumerWidget { actionsIconTheme: const IconThemeData( size: iconSize, ), + shape: const Border(), actions: [ if (asset.isRemote && isOwner) buildFavoriteButton(a), if (asset.livePhotoVideoId != null) buildLivePhotoButton(), diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 2e10fe0b75..7cdc595c7f 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -47,22 +47,22 @@ class AlbumInfoListTile extends HookConsumerWidget { buildIcon() { if (isSelected) { - return const Icon( + return Icon( Icons.check_circle_rounded, - color: Colors.green, + color: context.colorScheme.primary, ); } if (isExcluded) { - return const Icon( + return Icon( Icons.remove_circle_rounded, - color: Colors.red, + color: context.colorScheme.error, ); } return Icon( Icons.circle, - color: context.isDarkTheme ? Colors.grey[400] : Colors.black45, + color: context.colorScheme.surfaceContainerHighest, ); } diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index e1b56a970a..58fc89cb65 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class BackupInfoCard extends StatelessWidget { final String title; @@ -19,9 +20,7 @@ class BackupInfoCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), // if you need this side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 56, 56, 56) - : Colors.black12, + color: context.colorScheme.outlineVariant, width: 1, ), ), @@ -38,7 +37,9 @@ class BackupInfoCard extends StatelessWidget { padding: const EdgeInsets.only(top: 4.0, right: 18.0), child: Text( subtitle, - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), ), trailing: Column( diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart index 2520acedf1..8e58905aaa 100644 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart @@ -7,6 +7,7 @@ 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; @@ -82,22 +83,20 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { Widget buildAssetInfoTable() { return Table( border: TableBorder.all( - color: context.themeData.primaryColorLight, + color: context.colorScheme.outlineVariant, width: 1, ), children: [ TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[100], - ), children: [ TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( 'backup_controller_page_filename', style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -109,17 +108,15 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ], ), TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[200], - ), children: [ TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( "backup_controller_page_created", style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -131,16 +128,14 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ], ), TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[100], - ), children: [ TableCell( child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( "backup_controller_page_id", style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -181,8 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { child: LinearProgressIndicator( minHeight: 10.0, value: uploadProgress / 100.0, - backgroundColor: Colors.grey, - color: context.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), Text( @@ -214,8 +208,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { child: LinearProgressIndicator( minHeight: 10.0, value: uploadProgress / 100.0, - backgroundColor: Colors.grey, - color: context.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), Text( diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index fbcfd64713..5b6e60b1db 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -57,6 +57,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png', height: 16, + color: context.primaryColor, ), ), ], @@ -88,7 +89,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { buildSettingButton() { return buildActionButton( - Icons.settings_rounded, + Icons.settings_outlined, "profile_drawer_settings", () => context.pushRoute(const SettingsRoute()), ); @@ -146,9 +147,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, ), child: ListTile( minLeadingWidth: 50, @@ -171,10 +170,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(top: 8.0), child: LinearProgressIndicator( - minHeight: 5.0, + minHeight: 10.0, value: percentage, - backgroundColor: Colors.grey, - color: theme.primaryColor, + borderRadius: + const BorderRadius.all(Radius.circular(10.0)), ), ), Padding( @@ -248,7 +247,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { right: horizontalPadding, bottom: isHorizontal ? 20 : 100, ), - backgroundColor: theme.cardColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index 5e768f3241..a40dcf914e 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -79,9 +80,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { child: Container( width: double.infinity, decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), @@ -99,9 +98,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { bottom: -5, right: -8, child: Material( - color: context.isDarkTheme - ? Colors.blueGrey[800] - : Colors.white, + color: context.colorScheme.surfaceContainerHighest, elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50.0), @@ -129,7 +126,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { subtitle: Text( authState.userEmail, style: context.textTheme.bodySmall?.copyWith( - color: context.textTheme.bodySmall?.color?.withAlpha(200), + color: context.colorScheme.onSurfaceSecondary, ), ), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 0beb45c49f..8cab0bd72f 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -42,9 +43,7 @@ class AppBarServerInfo extends HookConsumerWidget { padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), child: Container( decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10), @@ -71,10 +70,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -100,8 +96,7 @@ class AppBarServerInfo extends HookConsumerWidget { "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), @@ -111,10 +106,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -142,8 +134,7 @@ class AppBarServerInfo extends HookConsumerWidget { : "--", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), @@ -153,10 +144,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -197,8 +185,7 @@ class AppBarServerInfo extends HookConsumerWidget { getServerUrl() ?? '--', style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), @@ -211,10 +198,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -255,8 +239,7 @@ class AppBarServerInfo extends HookConsumerWidget { : "--", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), diff --git a/mobile/lib/widgets/common/confirm_dialog.dart b/mobile/lib/widgets/common/confirm_dialog.dart index 5f24f75d51..5e043cf8de 100644 --- a/mobile/lib/widgets/common/confirm_dialog.dart +++ b/mobile/lib/widgets/common/confirm_dialog.dart @@ -47,7 +47,7 @@ class ConfirmDialog extends StatelessWidget { child: Text( ok, style: TextStyle( - color: Colors.red[400], + color: context.colorScheme.error, fontWeight: FontWeight.bold, ), ).tr(), diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index a3b3a19f34..30802a435a 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -111,7 +111,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { buildBackupIndicator() { final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = isDarkTheme ? Colors.blueGrey[800] : Colors.white; + final badgeBackground = context.colorScheme.surfaceContainer; return InkWell( onTap: () => context.pushRoute(const BackupControllerRoute()), @@ -123,7 +123,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { decoration: BoxDecoration( color: badgeBackground, border: Border.all( - color: isDarkTheme ? Colors.black : Colors.grey, + color: context.colorScheme.outline.withOpacity(.3), ), borderRadius: BorderRadius.circular(widgetSize / 2), ), diff --git a/mobile/lib/widgets/common/immich_title_text.dart b/mobile/lib/widgets/common/immich_title_text.dart index 2a4edb4230..711d0bf396 100644 --- a/mobile/lib/widgets/common/immich_title_text.dart +++ b/mobile/lib/widgets/common/immich_title_text.dart @@ -21,6 +21,7 @@ class ImmichTitleText extends StatelessWidget { ), width: fontSize * 4, filterQuality: FilterQuality.high, + color: context.primaryColor, ); } } diff --git a/mobile/lib/widgets/common/immich_toast.dart b/mobile/lib/widgets/common/immich_toast.dart index e15623c86c..d33f6c4caf 100644 --- a/mobile/lib/widgets/common/immich_toast.dart +++ b/mobile/lib/widgets/common/immich_toast.dart @@ -51,9 +51,9 @@ class ImmichToast { padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0), - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[50], + color: context.colorScheme.surfaceContainer, border: Border.all( - color: Colors.black12, + color: context.colorScheme.outline.withOpacity(.5), width: 1, ), ), diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 0d1ac539dc..98ce66d2d1 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -51,7 +51,7 @@ class ChangePasswordForm extends HookConsumerWidget { ), style: TextStyle( fontSize: 14, - color: Colors.grey[700], + color: context.colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), @@ -191,9 +191,6 @@ class ChangePasswordButton extends ConsumerWidget { return ElevatedButton( style: ElevatedButton.styleFrom( visualDensity: VisualDensity.standard, - backgroundColor: context.primaryColor, - foregroundColor: Colors.grey[50], - elevation: 2, padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), onPressed: onPressed, diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index f56942c69c..3b66a1cc35 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -70,6 +70,7 @@ class _MapThemeOverideState extends ConsumerState Widget build(BuildContext context) { _theme = widget.themeMode ?? ref.watch(mapStateNotifierProvider.select((v) => v.themeMode)); + var appTheme = ref.watch(immichThemeProvider); useValueChanged(_theme, (_, __) { if (_theme == ThemeMode.system) { @@ -83,7 +84,9 @@ class _MapThemeOverideState extends ConsumerState }); return Theme( - data: _isDarkTheme ? immichDarkTheme : immichLightTheme, + data: _isDarkTheme + ? getThemeData(colorScheme: appTheme.dark) + : getThemeData(colorScheme: appTheme.light), child: widget.mapBuilder.call( ref.watch( mapStateNotifierProvider.select( diff --git a/mobile/lib/widgets/memories/memory_epilogue.dart b/mobile/lib/widgets/memories/memory_epilogue.dart index b817d67f05..9796bee6b1 100644 --- a/mobile/lib/widgets/memories/memory_epilogue.dart +++ b/mobile/lib/widgets/memories/memory_epilogue.dart @@ -1,6 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; class MemoryEpilogue extends StatefulWidget { @@ -49,24 +48,26 @@ class _MemoryEpilogueState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.check_circle_outline_sharp, - color: immichDarkThemePrimaryColor, + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, size: 64.0, ), const SizedBox(height: 16.0), Text( "memories_all_caught_up", - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.headlineMedium?.copyWith( + color: Colors.white, + ), ).tr(), const SizedBox(height: 16.0), Text( "memories_check_back_tomorrow", - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), ).tr(), const SizedBox(height: 16.0), TextButton( @@ -74,7 +75,9 @@ class _MemoryEpilogueState extends State child: Text( "memories_start_over", style: context.textTheme.displayMedium?.copyWith( - color: immichDarkThemePrimaryColor, + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, ), ).tr(), ), @@ -108,9 +111,9 @@ class _MemoryEpilogueState extends State ), Text( "memories_swipe_to_close", - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), ).tr(), ], ), diff --git a/mobile/lib/widgets/memories/memory_progress_indicator.dart b/mobile/lib/widgets/memories/memory_progress_indicator.dart index 0ee3893cb9..438816d99c 100644 --- a/mobile/lib/widgets/memories/memory_progress_indicator.dart +++ b/mobile/lib/widgets/memories/memory_progress_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class MemoryProgressIndicator extends StatelessWidget { /// The number of ticks in the progress indicator @@ -25,8 +25,11 @@ class MemoryProgressIndicator extends StatelessWidget { children: [ LinearProgressIndicator( value: value, - backgroundColor: Colors.grey[600], - color: immichDarkThemePrimaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + backgroundColor: Colors.grey[800], + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart index b2e0d086ac..7db2eea70b 100644 --- a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart +++ b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart @@ -22,9 +22,9 @@ class SearchFilterChip extends StatelessWidget { onTap: onTap, child: Card( elevation: 0, - color: context.primaryColor.withAlpha(25), + color: context.primaryColor.withOpacity(.5), shape: StadiumBorder( - side: BorderSide(color: context.primaryColor), + side: BorderSide(color: context.colorScheme.secondaryContainer), ), child: Padding( padding: @@ -47,8 +47,9 @@ class SearchFilterChip extends StatelessWidget { onTap: onTap, child: Card( elevation: 0, - shape: - StadiumBorder(side: BorderSide(color: Colors.grey.withAlpha(100))), + shape: StadiumBorder( + side: BorderSide(color: context.colorScheme.outline.withOpacity(.5)), + ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), child: Row( diff --git a/mobile/lib/widgets/search/thumbnail_with_info_container.dart b/mobile/lib/widgets/search/thumbnail_with_info_container.dart index 6df45ec464..d2084bdcc8 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info_container.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info_container.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class ThumbnailWithInfoContainer extends StatelessWidget { const ThumbnailWithInfoContainer({ @@ -25,7 +26,14 @@ class ThumbnailWithInfoContainer extends StatelessWidget { Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], + gradient: LinearGradient( + colors: [ + context.colorScheme.surfaceContainer, + context.colorScheme.surfaceContainer.darken(amount: .1), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), ), foregroundDecoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), @@ -34,7 +42,7 @@ class ThumbnailWithInfoContainer extends StatelessWidget { begin: FractionalOffset.topCenter, end: FractionalOffset.bottomCenter, colors: [ - Colors.grey.withOpacity(0.0), + Colors.transparent, label == '' ? Colors.black.withOpacity(0.1) : Colors.black.withOpacity(0.5), diff --git a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart index 12efa52b2d..2e1f165602 100644 --- a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart +++ b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; class CustomeProxyHeaderSettings extends StatelessWidget { @@ -20,8 +21,8 @@ class CustomeProxyHeaderSettings extends StatelessWidget { ), subtitle: Text( "headers_settings_tile_subtitle".tr(), - style: const TextStyle( - fontSize: 14, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, ), ), onTap: () => context.pushRoute(const HeaderSettingsRoute()), diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 378d32085e..990dcfdfe8 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -40,9 +40,7 @@ class LanguageSettings extends HookConsumerWidget { ), ), backgroundColor: WidgetStatePropertyAll( - context.isDarkTheme - ? Colors.grey[900]! - : context.scaffoldBackgroundColor, + context.colorScheme.surfaceContainer, ), ), menuHeight: context.height * 0.5, diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart index 6e7723cbff..5b21d9bd4d 100644 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ b/mobile/lib/widgets/settings/local_storage_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/db.provider.dart'; class LocalStorageSettings extends HookConsumerWidget { @@ -35,10 +36,10 @@ class LocalStorageSettings extends HookConsumerWidget { fontWeight: FontWeight.w500, ), ).tr(args: ["${cacheItemCount.value}"]), - subtitle: const Text( + subtitle: Text( "cache_settings_duplicated_assets_subtitle", - style: TextStyle( - fontSize: 14, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, ), ).tr(), trailing: TextButton( diff --git a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart index 62508df6e2..8a3684e093 100644 --- a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart @@ -15,6 +15,9 @@ class PreferenceSetting extends StatelessWidget { HapticSetting(), ]; - return const SettingsSubPageScaffold(settings: preferenceSettings); + return const SettingsSubPageScaffold( + settings: preferenceSettings, + showDivider: true, + ); } } diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart new file mode 100644 index 0000000000..1c7cd1f207 --- /dev/null +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -0,0 +1,221 @@ +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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.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'; + +class PrimaryColorSetting extends HookConsumerWidget { + const PrimaryColorSetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeProvider = ref.read(immichThemeProvider); + + final primaryColorSetting = + useAppSettingsState(AppSettingsEnum.primaryColor); + final systemPrimaryColorSetting = + useAppSettingsState(AppSettingsEnum.dynamicTheme); + + final currentPreset = useValueNotifier(ref.read(immichThemePresetProvider)); + const tileSize = 55.0; + + useValueChanged( + primaryColorSetting.value, + (_, __) => currentPreset.value = ImmichColorPreset.values + .firstWhere((e) => e.name == primaryColorSetting.value), + ); + + void popBottomSheet() { + Future.delayed(const Duration(milliseconds: 200), () { + Navigator.pop(context); + }); + } + + onUseSystemColorChange(bool newValue) { + systemPrimaryColorSetting.value = newValue; + ref.watch(dynamicThemeSettingProvider.notifier).state = newValue; + ref.invalidate(immichThemeProvider); + popBottomSheet(); + } + + onPrimaryColorChange(ImmichColorPreset colorPreset) { + primaryColorSetting.value = colorPreset.name; + ref.watch(immichThemePresetProvider.notifier).state = colorPreset; + ref.invalidate(immichThemeProvider); + + //turn off system color setting + if (systemPrimaryColorSetting.value) { + onUseSystemColorChange(false); + } else { + popBottomSheet(); + } + } + + buildPrimaryColorTile({ + required Color topColor, + required Color bottomColor, + required double tileSize, + required bool showSelector, + }) { + return Container( + margin: const EdgeInsets.all(4.0), + child: Stack( + children: [ + Container( + height: tileSize, + width: tileSize, + decoration: BoxDecoration( + color: bottomColor, + borderRadius: const BorderRadius.all(Radius.circular(100)), + ), + ), + Container( + height: tileSize / 2, + width: tileSize, + decoration: BoxDecoration( + color: topColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(100), + topRight: Radius.circular(100), + ), + ), + ), + if (showSelector) + Positioned( + left: 0, + right: 0, + top: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(100)), + color: Colors.grey[900]?.withOpacity(.4), + ), + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon( + Icons.check_rounded, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ], + ), + ); + } + + bottomSheetContent() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.center, + child: Text( + "theme_setting_primary_color_title".tr(), + style: context.textTheme.titleLarge, + ), + ), + if (isDynamicThemeAvailable) + Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + margin: const EdgeInsets.only(top: 10), + child: SwitchListTile.adaptive( + contentPadding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 20), + dense: true, + activeColor: context.primaryColor, + tileColor: context.colorScheme.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + title: Text( + 'theme_setting_system_primary_color_title'.tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + value: systemPrimaryColorSetting.value, + onChanged: onUseSystemColorChange, + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: ImmichColorPreset.values.map((themePreset) { + var theme = themePreset.getTheme(); + + return GestureDetector( + onTap: () => onPrimaryColorChange(themePreset), + child: buildPrimaryColorTile( + topColor: theme.light.primary, + bottomColor: theme.dark.primary, + tileSize: tileSize, + showSelector: currentPreset.value == themePreset && + !systemPrimaryColorSetting.value, + ), + ); + }).toList(), + ), + ), + ], + ); + } + + return ListTile( + onTap: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext ctx) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 0), + child: bottomSheetContent(), + ); + }, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + title: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "theme_setting_primary_color_title".tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Text( + "theme_setting_primary_color_subtitle".tr(), + style: context.textTheme.bodyMedium + ?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 8.0), + child: buildPrimaryColorTile( + topColor: themeProvider.light.primary, + bottomColor: themeProvider.dark.primary, + tileSize: 42.0, + showSelector: false, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 5780054428..050593a229 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.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'; @@ -16,11 +17,16 @@ class ThemeSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode); - final currentTheme = useValueNotifier(ref.read(immichThemeProvider)); + final currentTheme = useValueNotifier(ref.read(immichThemeModeProvider)); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); + final applyThemeToBackgroundSetting = + useAppSettingsState(AppSettingsEnum.colorfulInterface); + final applyThemeToBackgroundProvider = + useValueNotifier(ref.read(colorfulInterfaceSettingProvider)); + useValueChanged( currentThemeString.value, (_, __) => currentTheme.value = switch (currentThemeString.value) { @@ -30,12 +36,18 @@ class ThemeSetting extends HookConsumerWidget { }, ); + useValueChanged( + applyThemeToBackgroundSetting.value, + (_, __) => applyThemeToBackgroundProvider.value = + applyThemeToBackgroundSetting.value, + ); + void onThemeChange(bool isDark) { if (isDark) { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; currentThemeString.value = "dark"; } else { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; currentThemeString.value = "light"; } } @@ -44,7 +56,7 @@ class ThemeSetting extends HookConsumerWidget { if (isSystem) { currentThemeString.value = "system"; isSystemTheme.value = true; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.system; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system; } else { final currentSystemBrightness = MediaQuery.platformBrightnessOf(context); @@ -52,14 +64,20 @@ class ThemeSetting extends HookConsumerWidget { isDarkTheme.value = currentSystemBrightness == Brightness.dark; if (currentSystemBrightness == Brightness.light) { currentThemeString.value = "light"; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; } else if (currentSystemBrightness == Brightness.dark) { currentThemeString.value = "dark"; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; } } } + void onSurfaceColorSettingChange(bool useColorfulInterface) { + applyThemeToBackgroundSetting.value = useColorfulInterface; + ref.watch(colorfulInterfaceSettingProvider.notifier).state = + useColorfulInterface; + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -75,6 +93,13 @@ class ThemeSetting extends HookConsumerWidget { title: 'theme_setting_dark_mode_switch'.tr(), onChanged: onThemeChange, ), + const PrimaryColorSetting(), + SettingsSwitchListTile( + valueNotifier: applyThemeToBackgroundProvider, + title: "theme_setting_colorful_interface_title".tr(), + subtitle: 'theme_setting_colorful_interface_subtitle'.tr(), + onChanged: onSurfaceColorSettingChange, + ), ], ); } diff --git a/mobile/lib/widgets/settings/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart index fca5b878de..196e3d170f 100644 --- a/mobile/lib/widgets/settings/settings_button_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class SettingsButtonListTile extends StatelessWidget { final IconData icon; @@ -39,7 +40,12 @@ class SettingsButtonListTile extends StatelessWidget { children: [ if (subtileText != null) const SizedBox(height: 4), if (subtileText != null) - Text(subtileText!, style: context.textTheme.bodyMedium), + Text( + subtileText!, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), if (subtitle != null) subtitle!, const SizedBox(height: 6), ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index c7328f0b96..78f1738266 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class SettingsSwitchListTile extends StatelessWidget { final ValueNotifier valueNotifier; @@ -54,7 +55,9 @@ class SettingsSwitchListTile extends StatelessWidget { ? Text( subtitle!, style: context.textTheme.bodyMedium?.copyWith( - color: enabled ? null : context.themeData.disabledColor, + color: enabled + ? context.colorScheme.onSurfaceSecondary + : context.themeData.disabledColor, ), ) : null, diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index 0daddd6d88..21d9738b84 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; class SslClientCertSettings extends StatefulWidget { @@ -40,7 +41,9 @@ class _SslClientCertSettingsState extends State { children: [ Text( "client_cert_subtitle".tr(), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), const SizedBox( height: 6, diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index 86c0890cd2..9e29f5f9a0 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -65,8 +65,8 @@ class SharedLinkItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeData = context.themeData; - final isDarkMode = themeData.brightness == Brightness.dark; + final colorScheme = context.colorScheme; + final isDarkMode = colorScheme.brightness == Brightness.dark; final thumbnailUrl = sharedLink.thumbAssetId != null ? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!) : null; @@ -159,7 +159,7 @@ class SharedLinkItem extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(right: 10), child: Chip( - backgroundColor: themeData.primaryColor, + backgroundColor: colorScheme.primary, label: Text( labelText, style: TextStyle( @@ -240,7 +240,7 @@ class SharedLinkItem extends ConsumerWidget { child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( - color: themeData.primaryColor.withOpacity(0.9), + color: colorScheme.primary.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( @@ -253,7 +253,7 @@ class SharedLinkItem extends ConsumerWidget { child: Text( sharedLink.title, style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), @@ -268,7 +268,7 @@ class SharedLinkItem extends ConsumerWidget { child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( - color: themeData.primaryColor.withOpacity(0.9), + color: colorScheme.primary.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index c7e397999c..8d5a912a51 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -377,6 +377,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" easy_image_viewer: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c830707182..9b74bec14c 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -61,9 +61,11 @@ dependencies: octo_image: ^2.0.0 thumbhash: 0.1.0+1 async: ^2.11.0 + dynamic_color: ^1.7.0 #package to apply system theme #image editing packages crop_image: ^1.0.13 + openapi: path: openapi