From c0db0f196ac03aac155278640eb176c33340b794 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Dec 2024 15:02:25 +0100 Subject: [PATCH] More work on custom theme --- lib/src/app.dart | 34 +- lib/src/constants.dart | 2 + lib/src/init.dart | 44 +- .../model/settings/general_preferences.dart | 23 +- .../view/settings/settings_tab_screen.dart | 2 - lib/src/view/settings/theme_screen.dart | 653 ++++++++++-------- 6 files changed, 437 insertions(+), 321 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 40cf12ec4c..a7740b476f 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -17,7 +17,6 @@ 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'; @@ -140,27 +139,22 @@ class _AppState extends ConsumerState { final dynamicColorScheme = brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme; - 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( + final ColorScheme colorScheme = switch (generalPrefs.appThemeSeed) { + AppThemeSeed.color => ColorScheme.fromSeed( + seedColor: generalPrefs.customThemeSeed ?? kDefaultSeedColor, + brightness: brightness, + ), + AppThemeSeed.board => ColorScheme.fromSeed( seedColor: boardTheme.colors.darkSquare, brightness: brightness, - ); - } + ), + AppThemeSeed.system => + dynamicColorScheme ?? + ColorScheme.fromSeed( + seedColor: boardTheme.colors.darkSquare, + brightness: brightness, + ), + }; final cupertinoThemeData = CupertinoThemeData( primaryColor: colorScheme.primary, diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 240db4322b..5aa82a7dce 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -37,6 +37,8 @@ const kClueLessDeviation = 230; // UI +const kDefaultSeedColor = Color(0xFFD64F00); + const kGoldenRatio = 1.61803398875; /// Flex golden ratio base (flex has to be an int). diff --git a/lib/src/init.dart b/lib/src/init.dart index d4ac98c44f..3c47e67ed6 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -32,6 +32,11 @@ Future setupFirstLaunch() async { final appVersion = Version.parse(pInfo.version); final installedVersion = prefs.getString('installed_version'); + // TODO remove this migration code after a few releases + if (installedVersion != null && Version.parse(installedVersion) <= Version(0, 13, 9)) { + _migrateThemeSettings(); + } + if (installedVersion == null || Version.parse(installedVersion) != appVersion) { prefs.setString('installed_version', appVersion.canonicalizedVersion); } @@ -49,6 +54,26 @@ Future setupFirstLaunch() async { } } +Future _migrateThemeSettings() async { + final prefs = LichessBinding.instance.sharedPreferences; + try { + final stored = LichessBinding.instance.sharedPreferences.getString( + PrefCategory.general.storageKey, + ); + if (stored == null) { + return; + } + final generalPrefs = GeneralPrefs.fromJson(jsonDecode(stored) as Map); + final migrated = generalPrefs.copyWith( + // ignore: deprecated_member_use_from_same_package + appThemeSeed: generalPrefs.systemColors == true ? AppThemeSeed.system : AppThemeSeed.board, + ); + await prefs.setString(PrefCategory.general.storageKey, jsonEncode(migrated.toJson())); + } catch (e) { + _logger.warning('Failed to migrate theme settings: $e'); + } +} + Future initializeLocalNotifications(Locale locale) async { final l10n = await AppLocalizations.delegate.load(locale); await FlutterLocalNotificationsPlugin().initialize( @@ -86,27 +111,10 @@ Future preloadPieceImages() async { /// /// This is meant to be called once during app initialization. Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { - final prefs = LichessBinding.instance.sharedPreferences; - - // On android 12+ get core palette and set the board theme to system if it is not set + // On android 12+ set core palette and make system board try { await DynamicColorPlugin.getCorePalette().then((value) { setCorePalette(value); - - 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) { _logger.fine('Device does not support core palette: $e'); diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index f788e60dd5..aa9571ee6c 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,7 +1,6 @@ import 'dart:ui' show Color, Locale; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -54,6 +53,10 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage setCustomThemeSeed(Color? color) { return save(state.copyWith(customThemeSeed: color)); } + + Future setAppThemeSeed(AppThemeSeed seed) { + return save(state.copyWith(appThemeSeed: seed)); + } } @Freezed(fromJson: true, toJson: true) @@ -71,6 +74,12 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { /// Custom theme seed color @ColorConverter() Color? customThemeSeed, + @Deprecated('Use appThemeSeed instead') bool? systemColors, + + /// App theme seed + @JsonKey(unknownEnumValue: AppThemeSeed.color, defaultValue: AppThemeSeed.color) + required AppThemeSeed appThemeSeed, + /// Locale to use in the app, use system locale if null @LocaleConverter() Locale? locale, }) = _GeneralPrefs; @@ -81,6 +90,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { soundTheme: SoundTheme.standard, masterVolume: 0.8, customThemeEnabled: false, + appThemeSeed: AppThemeSeed.color, ); factory GeneralPrefs.fromJson(Map json) { @@ -88,6 +98,17 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { } } +enum AppThemeSeed { + /// The app theme is based on the user's system theme (only available on Android 10+). + system, + + /// The app theme is based on the chessboard. + board, + + /// The app theme is based on a specific color. + color, +} + /// Describes the background theme of the app. enum BackgroundThemeMode { /// Use either the light or dark theme based on what the user has selected in diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 687952a257..6396d7287d 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/preloaded_data.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/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -82,7 +81,6 @@ class _Body extends ConsumerWidget { }); final generalPrefs = ref.watch(generalPreferencesProvider); - final boardPrefs = ref.watch(boardPreferencesProvider); final authController = ref.watch(authControllerProvider); final userSession = ref.watch(authSessionProvider); final packageInfo = ref.read(preloadedDataProvider).requireValue.packageInfo; diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 10d58c8748..c4f3a516b5 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -1,34 +1,51 @@ -import 'dart:math' as math; +import 'dart:ui' show ImageFilter; import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.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/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.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/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.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/change_colors.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; -import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; +const _kBoardSize = 200.0; + class ThemeScreen extends StatelessWidget { const ThemeScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformScaffold(appBar: const PlatformAppBar(title: Text('Theme')), body: _Body()); + return PlatformWidget( + androidBuilder: (context) => const Scaffold(body: _Body()), + iosBuilder: + (context) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Styles.cupertinoAppBarColor + .resolveFrom(context) + .withValues(alpha: 0.0), + border: null, + ), + child: const _Body(), + ), + ); } } @@ -42,6 +59,8 @@ switch (shapeColor) { }; class _Body extends ConsumerStatefulWidget { + const _Body(); + @override ConsumerState<_Body> createState() => _BodyState(); } @@ -50,6 +69,10 @@ class _BodyState extends ConsumerState<_Body> { late double brightness; late double hue; + double headerOpacity = 0; + + bool openAdjustColorSection = false; + @override void initState() { super.initState(); @@ -58,220 +81,296 @@ class _BodyState extends ConsumerState<_Body> { hue = boardPrefs.hue; } + bool handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && notification.depth == 0) { + final ScrollMetrics metrics = notification.metrics; + double scrollExtent = 0.0; + switch (metrics.axisDirection) { + case AxisDirection.up: + scrollExtent = metrics.extentAfter; + case AxisDirection.down: + scrollExtent = metrics.extentBefore; + case AxisDirection.right: + case AxisDirection.left: + break; + } + + final opacity = scrollExtent > 0.0 ? 1.0 : 0.0; + + if (opacity != headerOpacity) { + setState(() { + headerOpacity = opacity; + }); + } + } + return false; + } + + void _showColorPicker() { + final generalPrefs = ref.read(generalPreferencesProvider); + showAdaptiveDialog( + context: context, + barrierDismissible: false, + builder: (context) { + bool useDefault = generalPrefs.customThemeSeed == null; + Color color = generalPrefs.customThemeSeed ?? kDefaultSeedColor; + 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: 'Default color', + onPressed: + !useDefault + ? () { + setState(() { + useDefault = true; + color = kDefaultSeedColor; + }); + } + : null, + child: const Text('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?); + } + }); + } + @override Widget build(BuildContext context) { final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); - const horizontalPadding = 16.0; - final bool hasAjustedColors = brightness != 0.0 || hue != 0.0; - return ListView( - children: [ - LayoutBuilder( - builder: (context, constraints) { - final double boardSize = math.min( - 250, - constraints.biggest.shortestSide - horizontalPadding * 2, - ); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: 16), - child: Center( - child: ChangeColors( - brightness: brightness, - hue: hue, - child: Chessboard.fixed( - size: boardSize, - orientation: Side.white, - lastMove: const NormalMove(from: Square.e2, to: Square.e4), - fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', - shapes: - { - Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b8'), - dest: Square.fromName('c6'), - ), - }.lock, - settings: boardPrefs.toBoardSettings().copyWith( - brightness: 0.0, - hue: 0.0, - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, + final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); + + return NotificationListener( + onNotification: handleScrollNotification, + child: CustomScrollView( + slivers: [ + if (Theme.of(context).platform == TargetPlatform.iOS) + PinnedHeaderSliver( + child: ClipRect( + child: BackdropFilter( + enabled: backgroundColor.alpha != 0xFF, + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: ShapeDecoration( + color: headerOpacity == 1.0 ? backgroundColor : backgroundColor.withAlpha(0), + shape: LinearBorder.bottom( + side: BorderSide( + color: + headerOpacity == 1.0 ? const Color(0x4D000000) : Colors.transparent, + width: 0.0, + ), + ), ), + padding: + Styles.bodyPadding + + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + child: _BoardPreview(boardPrefs: boardPrefs, brightness: brightness, hue: hue), ), ), ), - ); - }, - ), - ListSection( - hasLeading: true, - children: [ - PlatformListTile( - leading: const Icon(Icons.brightness_6), - title: Slider.adaptive( - min: -0.5, - max: 0.5, - value: brightness, - onChanged: (value) { - setState(() { - brightness = value; - }); - }, - onChangeEnd: (value) { - ref.read(boardPreferencesProvider.notifier).adjustColors(brightness: brightness); - }, - ), - ), - PlatformListTile( - leading: const Icon(Icons.invert_colors), - title: Slider.adaptive( - min: -1.0, - max: 1.0, - value: hue, - onChanged: (value) { - setState(() { - hue = value; - }); - }, - onChangeEnd: (value) { - ref.read(boardPreferencesProvider.notifier).adjustColors(hue: hue); - }, + ) + else + SliverAppBar( + pinned: true, + title: const Text('Theme'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(_kBoardSize + 16.0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: _BoardPreview(boardPrefs: boardPrefs, brightness: brightness, hue: hue), + ), ), ), - PlatformListTile( - leading: Opacity( - opacity: hasAjustedColors ? 1.0 : 0.5, - child: const Icon(Icons.cancel), - ), - title: Opacity( - opacity: hasAjustedColors ? 1.0 : 0.5, - child: Text(context.l10n.boardReset), + SliverList.list( + children: [ + ListSection( + hasLeading: true, + children: [ + SettingsListTile( + icon: const Icon(LichessIcons.chess_board), + settingsLabel: Text(context.l10n.board), + settingsValue: boardPrefs.boardTheme.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.board, + builder: (context) => const BoardThemeScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.chess_pawn), + settingsLabel: Text(context.l10n.pieceSet), + settingsValue: boardPrefs.pieceSet.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.pieceSet, + builder: (context) => const PieceSetScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.arrow_full_upperright), + settingsLabel: const Text('Shape color'), + settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), + onTap: () { + showChoicePicker( + context, + choices: ShapeColor.values, + selectedItem: boardPrefs.shapeColor, + labelBuilder: + (t) => Text.rich( + TextSpan( + children: [ + TextSpan(text: shapeColorL10n(context, t)), + const TextSpan(text: ' '), + WidgetSpan( + child: Container(width: 15, height: 15, color: t.color), + ), + ], + ), + ), + onSelectedItemChanged: (ShapeColor? value) { + ref + .read(boardPreferencesProvider.notifier) + .setShapeColor(value ?? ShapeColor.green); + }, + ); + }, + ), + SwitchSettingTile( + leading: const Icon(Icons.location_on), + title: Text(context.l10n.preferencesBoardCoordinates), + value: boardPrefs.coordinates, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); + }, + ), + SwitchSettingTile( + // TODO translate + leading: const Icon(Icons.border_outer), + title: const Text('Show border'), + value: boardPrefs.showBorder, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleBorder(); + }, + ), + ], ), - onTap: - hasAjustedColors - ? () { + ListSection( + header: SettingsSectionTitle(context.l10n.advancedSettings), + hasLeading: true, + children: [ + PlatformListTile( + leading: const Icon(Icons.brightness_6), + title: Slider.adaptive( + min: -0.5, + max: 0.5, + value: brightness, + onChanged: (value) { setState(() { - brightness = 0.0; - hue = 0.0; + brightness = value; }); + }, + onChangeEnd: (value) { ref .read(boardPreferencesProvider.notifier) - .adjustColors(brightness: 0.0, hue: 0.0); - } - : null, - ), - ], - ), - ListSection( - hasLeading: true, - children: [ - SettingsListTile( - icon: const Icon(LichessIcons.chess_board), - settingsLabel: Text(context.l10n.board), - settingsValue: boardPrefs.boardTheme.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.board, - builder: (context) => const BoardThemeScreen(), - ); - }, - ), - SettingsListTile( - icon: const Icon(LichessIcons.chess_pawn), - settingsLabel: Text(context.l10n.pieceSet), - settingsValue: boardPrefs.pieceSet.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.pieceSet, - builder: (context) => const PieceSetScreen(), - ); - }, - ), - SettingsListTile( - icon: const Icon(LichessIcons.arrow_full_upperright), - settingsLabel: const Text('Shape color'), - settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), - onTap: () { - showChoicePicker( - context, - choices: ShapeColor.values, - selectedItem: boardPrefs.shapeColor, - labelBuilder: - (t) => Text.rich( - TextSpan( - children: [ - TextSpan(text: shapeColorL10n(context, t)), - const TextSpan(text: ' '), - WidgetSpan(child: Container(width: 15, height: 15, color: t.color)), - ], - ), - ), - onSelectedItemChanged: (ShapeColor? value) { - ref - .read(boardPreferencesProvider.notifier) - .setShapeColor(value ?? ShapeColor.green); - }, - ); - }, - ), - SwitchSettingTile( - leading: const Icon(Icons.location_on), - title: Text(context.l10n.preferencesBoardCoordinates), - value: boardPrefs.coordinates, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); - }, - ), - SwitchSettingTile( - // TODO translate - leading: const Icon(Icons.border_outer), - title: const Text('Show border'), - value: boardPrefs.showBorder, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleBorder(); - }, - ), - ], - ), - ListSection( - hasLeading: true, - children: [ - 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('App theme'), - // TODO translate - subtitle: const Text('Configure your own app theme using a seed color.', maxLines: 3), - value: generalPrefs.customThemeEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleCustomTheme(); - }, - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: - generalPrefs.customThemeEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: ListSection( - margin: EdgeInsets.zero, - cupertinoBorderRadius: BorderRadius.zero, - cupertinoClipBehavior: Clip.none, - children: [ + .adjustColors(brightness: brightness); + }, + ), + ), PlatformListTile( - leading: const Icon(Icons.color_lens), - title: const Text('Seed color'), - trailing: + leading: const Icon(Icons.invert_colors), + title: Slider.adaptive( + min: -1.0, + max: 1.0, + value: hue, + onChanged: (value) { + setState(() { + hue = value; + }); + }, + onChangeEnd: (value) { + ref.read(boardPreferencesProvider.notifier).adjustColors(hue: hue); + }, + ), + ), + PlatformListTile( + leading: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: const Icon(Icons.cancel), + ), + title: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: Text(context.l10n.boardReset), + ), + onTap: + hasAjustedColors + ? () { + setState(() { + brightness = 0.0; + hue = 0.0; + }); + ref + .read(boardPreferencesProvider.notifier) + .adjustColors(brightness: 0.0, hue: 0.0); + } + : null, + ), + PlatformListTile( + leading: const Icon(Icons.colorize_outlined), + title: const Text('App theme'), + trailing: switch (generalPrefs.appThemeSeed) { + AppThemeSeed.board => Text(context.l10n.board), + AppThemeSeed.system => Text(context.l10n.mobileSystemColors), + AppThemeSeed.color => generalPrefs.customThemeSeed != null ? Container( width: 20, @@ -281,102 +380,96 @@ class _BodyState extends ConsumerState<_Body> { shape: BoxShape.circle, ), ) - : getCorePalette() != null - ? Text(context.l10n.mobileSystemColors) : Container( width: 20, height: 20, - decoration: BoxDecoration( - color: LichessColors.primary[500], + decoration: const BoxDecoration( + color: kDefaultSeedColor, shape: BoxShape.circle, ), ), + }, onTap: () { - showAdaptiveDialog( + showAdaptiveActionSheet( context: context, - barrierDismissible: false, - builder: (context) { - 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); + actions: + AppThemeSeed.values + .where((t) => t != AppThemeSeed.system || getCorePalette() != null) + .map( + (t) => BottomSheetAction( + makeLabel: + (context) => switch (t) { + AppThemeSeed.board => Text(context.l10n.board), + AppThemeSeed.system => Text( + context.l10n.mobileSystemColors, + ), + AppThemeSeed.color => const Text('Custom color'), }, - 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), - ), - ], + onPressed: (context) { + ref + .read(generalPreferencesProvider.notifier) + .setAppThemeSeed(t); + + if (t == AppThemeSeed.color) { + _showColorPicker(); + } + }, + dismissOnPress: true, ), - ), - ); - }, - ); - }, - ).then((color) { - if (color != false) { - ref - .read(generalPreferencesProvider.notifier) - .setCustomThemeSeed(color as Color?); - } - }); + ) + .toList(), + ); }, ), ], ), - ), - ], + ], + ), + const SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter(child: SizedBox(height: 16.0)), + ), + ], + ), + ); + } +} + +class _BoardPreview extends StatelessWidget { + const _BoardPreview({required this.boardPrefs, required this.brightness, required this.hue}); + + final BoardPrefs boardPrefs; + final double brightness; + final double hue; + + @override + Widget build(BuildContext context) { + return Center( + child: ChangeColors( + brightness: brightness, + hue: hue, + child: Chessboard.fixed( + size: _kBoardSize, + orientation: Side.white, + lastMove: const NormalMove(from: Square.e2, to: Square.e4), + fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', + shapes: + { + Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b8'), + dest: Square.fromName('c6'), + ), + }.lock, + settings: boardPrefs.toBoardSettings().copyWith( + brightness: 0.0, + hue: 0.0, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + ), ), - ], + ), ); } }