From 52c1ded29ada1175fc69add8b0ba01f8a12641a6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Dec 2024 17:37:23 +0100 Subject: [PATCH] Implement custom theme --- lib/src/app.dart | 29 +++- lib/src/init.dart | 26 ++-- .../model/settings/general_preferences.dart | 24 ++-- lib/src/utils/color_palette.dart | 6 + lib/src/view/puzzle/puzzle_tab_screen.dart | 3 - .../settings/account_preferences_screen.dart | 1 - lib/src/view/settings/board_theme_screen.dart | 16 +-- lib/src/view/settings/theme_screen.dart | 128 +++++++++++++----- lib/src/widgets/settings.dart | 3 + 9 files changed, 155 insertions(+), 81 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 0cb59123d7..016035bfa4 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -147,13 +148,27 @@ class _AppState extends ConsumerState { final dynamicColorScheme = brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme; - final colorScheme = - generalPrefs.systemColors && dynamicColorScheme != null - ? dynamicColorScheme - : ColorScheme.fromSeed( - seedColor: boardTheme.colors.darkSquare, - brightness: brightness, - ); + ColorScheme colorScheme; + if (generalPrefs.customThemeEnabled) { + if (generalPrefs.customThemeSeed != null) { + colorScheme = ColorScheme.fromSeed( + seedColor: generalPrefs.customThemeSeed!, + brightness: brightness, + ); + } else if (dynamicColorScheme != null) { + colorScheme = dynamicColorScheme; + } else { + colorScheme = ColorScheme.fromSeed( + seedColor: LichessColors.primary[500]!, + brightness: brightness, + ); + } + } else { + colorScheme = ColorScheme.fromSeed( + seedColor: boardTheme.colors.darkSquare, + brightness: brightness, + ); + } final cupertinoThemeData = CupertinoThemeData( primaryColor: colorScheme.primary, diff --git a/lib/src/init.dart b/lib/src/init.dart index 788d8040b4..5f500bfbbe 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/utils/chessboard.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; @@ -95,14 +96,23 @@ Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { await DynamicColorPlugin.getCorePalette().then((value) { setCorePalette(value); - if (getCorePalette() != null && - prefs.getString(PrefCategory.board.storageKey) == null) { - prefs.setString( - PrefCategory.board.storageKey, - jsonEncode( - BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system), - ), - ); + if (getCorePalette() != null) { + if (prefs.getString(PrefCategory.general.storageKey) == null) { + prefs.setString( + PrefCategory.general.storageKey, + jsonEncode( + GeneralPrefs.defaults.copyWith(customThemeEnabled: true), + ), + ); + } + if (prefs.getString(PrefCategory.board.storageKey) == null) { + prefs.setString( + PrefCategory.board.storageKey, + jsonEncode( + BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system), + ), + ); + } } }); } catch (e) { diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index fb268cef72..f59a0fca7c 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -49,17 +49,9 @@ class GeneralPreferences extends _$GeneralPreferences return save(state.copyWith(masterVolume: volume)); } - Future toggleCustomTheme() { - return save(state.copyWith(customThemeEnabled: !state.customThemeEnabled)); - } - - Future setCustomThemeSeed(Color? color) { - return save(state.copyWith(customThemeSeed: color)); - } - - Future toggleSystemColors() async { - await save(state.copyWith(systemColors: !state.systemColors)); - if (state.systemColors == false) { + Future toggleCustomTheme() async { + await save(state.copyWith(customThemeEnabled: !state.customThemeEnabled)); + if (state.customThemeEnabled == false) { final boardTheme = ref.read(boardPreferencesProvider).boardTheme; if (boardTheme == BoardTheme.system) { await ref @@ -72,6 +64,10 @@ class GeneralPreferences extends _$GeneralPreferences .setBoardTheme(BoardTheme.system); } } + + Future setCustomThemeSeed(Color? color) { + return save(state.copyWith(customThemeSeed: color)); + } } @Freezed(fromJson: true, toJson: true) @@ -87,9 +83,6 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { required SoundTheme soundTheme, @JsonKey(defaultValue: 0.8) required double masterVolume, - /// Should enable system color palette (android 12+ only) - required bool systemColors, - /// Should enable custom theme @JsonKey(defaultValue: false) required bool customThemeEnabled, @@ -105,8 +98,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { isSoundEnabled: true, soundTheme: SoundTheme.standard, masterVolume: 0.8, - systemColors: true, - customThemeEnabled: false, + customThemeEnabled: true, ); factory GeneralPrefs.fromJson(Map json) { diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 4ecbb07e12..8b861920de 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -48,6 +48,12 @@ void setCorePalette(CorePalette? palette) { } } +Color? getCorePalettePrimary() { + return _corePalette?.primary != null + ? Color(_corePalette!.primary.get(50)) + : null; +} + /// Get the core palette if available (android 12+ only). CorePalette? getCorePalette() { return _corePalette; diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 43645c36b5..c45caa8263 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -423,9 +423,6 @@ class _PuzzleMenuListTile extends StatelessWidget { leading: Icon( icon, size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, ), title: Text(title, style: Styles.mainListTileTitle), subtitle: Text(subtitle, maxLines: 3), diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index f6797646d3..f624fea006 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -129,7 +129,6 @@ class _AccountPreferencesScreenState subtitle: Text( context.l10n.preferencesExplainShowPlayerRatings, maxLines: 5, - textAlign: TextAlign.justify, ), value: data.showRatings.value, onChanged: isLoading diff --git a/lib/src/view/settings/board_theme_screen.dart b/lib/src/view/settings/board_theme_screen.dart index c005919905..361adc81a3 100644 --- a/lib/src/view/settings/board_theme_screen.dart +++ b/lib/src/view/settings/board_theme_screen.dart @@ -2,9 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -40,20 +39,11 @@ class _Body extends ConsumerWidget { final boardTheme = ref.watch(boardPreferencesProvider.select((p) => p.boardTheme)); - final hasSystemColors = - ref.watch(generalPreferencesProvider.select((p) => p.systemColors)); - - final androidVersion = ref.watch(androidVersionProvider).whenOrNull( - data: (v) => v, - ); + final hasSystemColors = getCorePalette() != null; final choices = BoardTheme.values .where( - (t) => - t != BoardTheme.system || - (hasSystemColors && - androidVersion != null && - androidVersion.sdkInt >= 31), + (t) => t != BoardTheme.system || hasSystemColors, ) .toList(); diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index b45b4ababa..33ce24440a 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -10,12 +10,13 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; @@ -50,7 +51,6 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); - final androidVersionAsync = ref.watch(androidVersionProvider); const horizontalPadding = 16.0; @@ -98,25 +98,17 @@ class _Body extends ConsumerWidget { ListSection( hasLeading: true, children: [ - if (Theme.of(context).platform == TargetPlatform.android) - androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 31 - ? SwitchSettingTile( - leading: const Icon(Icons.colorize_outlined), - title: Text(context.l10n.mobileSystemColors), - value: generalPrefs.systemColors, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSystemColors(); - }, - ) - : const SizedBox.shrink(), - orElse: () => const SizedBox.shrink(), - ), SwitchSettingTile( leading: const Icon(Icons.colorize_outlined), + padding: Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14, vertical: 8) + : null, title: const Text('Custom theme'), + // TODO translate + subtitle: const Text( + 'Configure your own app theme using a seed color. Disable to use the chessboard theme.', + maxLines: 3, + ), value: generalPrefs.customThemeEnabled, onChanged: (value) { ref @@ -137,34 +129,104 @@ class _Body extends ConsumerWidget { children: [ PlatformListTile( leading: const Icon(Icons.color_lens), - title: const Text('Custom theme color'), + title: const Text('Seed color'), trailing: generalPrefs.customThemeSeed != null ? Container( width: 20, height: 20, decoration: BoxDecoration( - color: generalPrefs.customThemeSeed ?? - LichessColors.primary, + color: generalPrefs.customThemeSeed, shape: BoxShape.circle, ), ) - : null, + : getCorePalette() != null + ? Text(context.l10n.mobileSystemColors) + : Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: LichessColors.primary[500], + shape: BoxShape.circle, + ), + ), onTap: () { - showAdaptiveDialog( + showAdaptiveDialog( context: context, - barrierDismissible: true, + barrierDismissible: false, builder: (context) { - return PlatformAlertDialog( - content: SingleChildScrollView( - child: ColorPicker( - pickerColor: generalPrefs.customThemeSeed ?? - LichessColors.primary, - onColorChanged: (color) {}, - ), - ), + final defaultColor = getCorePalettePrimary() ?? + LichessColors.primary[500]!; + bool useDefault = + generalPrefs.customThemeSeed == null; + Color color = + generalPrefs.customThemeSeed ?? defaultColor; + return StatefulBuilder( + builder: (context, setState) { + return PlatformAlertDialog( + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ColorPicker( + enableAlpha: false, + pickerColor: color, + onColorChanged: (c) { + setState(() { + useDefault = false; + color = c; + }); + }, + ), + SecondaryButton( + semanticsLabel: getCorePalette() != null + ? context.l10n.mobileSystemColors + : 'Default color', + onPressed: !useDefault + ? () { + setState(() { + useDefault = true; + color = defaultColor; + }); + } + : null, + child: Text( + getCorePalette() != null + ? context.l10n.mobileSystemColors + : 'Default color', + ), + ), + SecondaryButton( + semanticsLabel: context.l10n.cancel, + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(context.l10n.cancel), + ), + SecondaryButton( + semanticsLabel: context.l10n.ok, + onPressed: () { + if (useDefault) { + Navigator.of(context).pop(null); + } else { + Navigator.of(context).pop(color); + } + }, + child: Text(context.l10n.ok), + ), + ], + ), + ), + ); + }, ); }, - ); + ).then((color) { + if (color != false) { + ref + .read(generalPreferencesProvider.notifier) + .setCustomThemeSeed(color as Color?); + } + }); }, ), ], diff --git a/lib/src/widgets/settings.dart b/lib/src/widgets/settings.dart index 2100a27873..a15ee65379 100644 --- a/lib/src/widgets/settings.dart +++ b/lib/src/widgets/settings.dart @@ -74,6 +74,7 @@ class SwitchSettingTile extends StatelessWidget { required this.value, this.onChanged, this.leading, + this.padding, super.key, }); @@ -82,10 +83,12 @@ class SwitchSettingTile extends StatelessWidget { final bool value; final void Function(bool value)? onChanged; final Widget? leading; + final EdgeInsetsGeometry? padding; @override Widget build(BuildContext context) { return PlatformListTile( + padding: padding, leading: leading, title: _SettingsTitle(title: title), subtitle: subtitle,