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