Skip to content

Commit

Permalink
Merge pull request #1251 from lichess-org/custom_theme
Browse files Browse the repository at this point in the history
Custom board colors and theme
  • Loading branch information
veloce authored Dec 19, 2024
2 parents ed56971 + 341755c commit bc90c63
Show file tree
Hide file tree
Showing 18 changed files with 690 additions and 266 deletions.
17 changes: 5 additions & 12 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ Future<void> main() async {

await lichessBinding.preloadSharedPreferences();

if (defaultTargetPlatform == TargetPlatform.android) {
await androidDisplayInitialization(widgetsBinding);
}

await preloadPieceImages();

await setupFirstLaunch();
Expand All @@ -38,18 +42,7 @@ Future<void> main() async {

await lichessBinding.initializeFirebase();

if (defaultTargetPlatform == TargetPlatform.android) {
await androidDisplayInitialization(widgetsBinding);
}

runApp(
ProviderScope(
observers: [
ProviderLogger(),
],
child: const AppInitializationScreen(),
),
);
runApp(ProviderScope(observers: [ProviderLogger()], child: const AppInitializationScreen()));
}

Future<void> migrateSharedPreferences() async {
Expand Down
19 changes: 14 additions & 5 deletions lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,22 @@ class _AppState extends ConsumerState<Application> {
final dynamicColorScheme =
brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme;

final colorScheme =
generalPrefs.systemColors && dynamicColorScheme != null
? dynamicColorScheme
: 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,
Expand Down
2 changes: 2 additions & 0 deletions lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const kClueLessDeviation = 230;

// UI

const kDefaultSeedColor = Color.fromARGB(255, 191, 128, 29);

const kGoldenRatio = 1.61803398875;

/// Flex golden ratio base (flex has to be an int).
Expand Down
41 changes: 31 additions & 10 deletions lib/src/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +32,11 @@ Future<void> 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);
}
Expand All @@ -48,6 +54,30 @@ Future<void> setupFirstLaunch() async {
}
}

Future<void> _migrateThemeSettings() async {
if (getCorePalette() == null) {
return;
}
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<String, dynamic>);
final migrated = generalPrefs.copyWith(
appThemeSeed:
// ignore: deprecated_member_use_from_same_package
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<void> initializeLocalNotifications(Locale locale) async {
final l10n = await AppLocalizations.delegate.load(locale);
await FlutterLocalNotificationsPlugin().initialize(
Expand Down Expand Up @@ -85,19 +115,10 @@ Future<void> preloadPieceImages() async {
///
/// This is meant to be called once during app initialization.
Future<void> 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 && 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');
Expand Down
17 changes: 17 additions & 0 deletions lib/src/model/settings/board_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'board_preferences.freezed.dart';
part 'board_preferences.g.dart';

const kBoardDefaultBrightnessFilter = 1.0;
const kBoardDefaultHueFilter = 0.0;

@riverpod
class BoardPreferences extends _$BoardPreferences with PreferencesStorage<BoardPrefs> {
// ignore: avoid_public_notifier_properties
Expand Down Expand Up @@ -94,12 +97,17 @@ class BoardPreferences extends _$BoardPreferences with PreferencesStorage<BoardP
Future<void> setShapeColor(ShapeColor shapeColor) {
return save(state.copyWith(shapeColor: shapeColor));
}

Future<void> adjustColors({double? brightness, double? hue}) {
return save(state.copyWith(brightness: brightness ?? state.brightness, hue: hue ?? state.hue));
}
}

@Freezed(fromJson: true, toJson: true)
class BoardPrefs with _$BoardPrefs implements Serializable {
const BoardPrefs._();

@Assert('brightness >= 0.2 && brightness <= 1.4, hue >= 0.0 && hue <= 360.0')
const factory BoardPrefs({
required PieceSet pieceSet,
required BoardTheme boardTheme,
Expand All @@ -126,6 +134,8 @@ class BoardPrefs with _$BoardPrefs implements Serializable {
@JsonKey(defaultValue: ShapeColor.green, unknownEnumValue: ShapeColor.green)
required ShapeColor shapeColor,
@JsonKey(defaultValue: false) required bool showBorder,
@JsonKey(defaultValue: kBoardDefaultBrightnessFilter) required double brightness,
@JsonKey(defaultValue: kBoardDefaultBrightnessFilter) required double hue,
}) = _BoardPrefs;

static const defaults = BoardPrefs(
Expand All @@ -145,12 +155,19 @@ class BoardPrefs with _$BoardPrefs implements Serializable {
dragTargetKind: DragTargetKind.circle,
shapeColor: ShapeColor.green,
showBorder: false,
brightness: kBoardDefaultBrightnessFilter,
hue: kBoardDefaultHueFilter,
);

bool get hasColorAdjustments =>
brightness != kBoardDefaultBrightnessFilter || hue != kBoardDefaultHueFilter;

ChessboardSettings toBoardSettings() {
return ChessboardSettings(
pieceAssets: pieceSet.assets,
colorScheme: boardTheme.colors,
brightness: brightness,
hue: hue,
border:
showBorder
? BoardBorder(color: darken(boardTheme.colors.darkSquare, 0.2), width: 16.0)
Expand Down
70 changes: 35 additions & 35 deletions lib/src/model/settings/general_preferences.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:ui' show Locale;
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';

part 'general_preferences.freezed.dart';
Expand All @@ -26,7 +26,7 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage<Ge
return fetch();
}

Future<void> setThemeMode(BackgroundThemeMode themeMode) {
Future<void> setBackgroundThemeMode(BackgroundThemeMode themeMode) {
return save(state.copyWith(themeMode: themeMode));
}

Expand All @@ -46,38 +46,17 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage<Ge
return save(state.copyWith(masterVolume: volume));
}

Future<void> toggleSystemColors() async {
await save(state.copyWith(systemColors: !state.systemColors));
if (state.systemColors == false) {
final boardTheme = ref.read(boardPreferencesProvider).boardTheme;
if (boardTheme == BoardTheme.system) {
await ref.read(boardPreferencesProvider.notifier).setBoardTheme(BoardTheme.brown);
}
} else {
await ref.read(boardPreferencesProvider.notifier).setBoardTheme(BoardTheme.system);
}
Future<void> toggleCustomTheme() async {
await save(state.copyWith(customThemeEnabled: !state.customThemeEnabled));
}
}

Map<String, dynamic>? _localeToJson(Locale? locale) {
return locale != null
? {
'languageCode': locale.languageCode,
'countryCode': locale.countryCode,
'scriptCode': locale.scriptCode,
}
: null;
}
Future<void> setCustomThemeSeed(Color? color) {
return save(state.copyWith(customThemeSeed: color));
}

Locale? _localeFromJson(Map<String, dynamic>? json) {
if (json == null) {
return null;
Future<void> setAppThemeSeed(AppThemeSeed seed) {
return save(state.copyWith(appThemeSeed: seed));
}
return Locale.fromSubtags(
languageCode: json['languageCode'] as String,
countryCode: json['countryCode'] as String?,
scriptCode: json['scriptCode'] as String?,
);
}

@Freezed(fromJson: true, toJson: true)
Expand All @@ -89,26 +68,47 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable {
@JsonKey(unknownEnumValue: SoundTheme.standard) required SoundTheme soundTheme,
@JsonKey(defaultValue: 0.8) required double masterVolume,

/// Should enable system color palette (android 12+ only)
@JsonKey(defaultValue: true) required bool systemColors,
/// Should enable custom theme
@JsonKey(defaultValue: false) required bool customThemeEnabled,

/// Custom theme seed color
@ColorConverter() Color? customThemeSeed,

@Deprecated('Use appThemeSeed instead') bool? systemColors,

/// App theme seed
@JsonKey(unknownEnumValue: AppThemeSeed.board, defaultValue: AppThemeSeed.board)
required AppThemeSeed appThemeSeed,

/// Locale to use in the app, use system locale if null
@JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale,
@LocaleConverter() Locale? locale,
}) = _GeneralPrefs;

static const defaults = GeneralPrefs(
themeMode: BackgroundThemeMode.system,
isSoundEnabled: true,
soundTheme: SoundTheme.standard,
masterVolume: 0.8,
systemColors: true,
customThemeEnabled: false,
appThemeSeed: AppThemeSeed.board,
);

factory GeneralPrefs.fromJson(Map<String, dynamic> json) {
return _$GeneralPrefsFromJson(json);
}
}

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
Expand Down
51 changes: 51 additions & 0 deletions lib/src/utils/json.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,57 @@
import 'dart:ui' show Color, Locale;

import 'package:deep_pick/deep_pick.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:lichess_mobile/src/model/common/uci.dart';

class LocaleConverter implements JsonConverter<Locale?, Map<String, dynamic>?> {
const LocaleConverter();

@override
Locale? fromJson(Map<String, dynamic>? json) {
if (json == null) {
return null;
}
return Locale.fromSubtags(
languageCode: json['languageCode'] as String,
countryCode: json['countryCode'] as String?,
scriptCode: json['scriptCode'] as String?,
);
}

@override
Map<String, dynamic>? toJson(Locale? locale) {
return locale != null
? {
'languageCode': locale.languageCode,
'countryCode': locale.countryCode,
'scriptCode': locale.scriptCode,
}
: null;
}
}

class ColorConverter implements JsonConverter<Color?, Map<String, dynamic>?> {
const ColorConverter();

@override
Color? fromJson(Map<String, dynamic>? json) {
return json != null
? Color.from(
alpha: json['a'] as double,
red: json['r'] as double,
green: json['g'] as double,
blue: json['b'] as double,
)
: null;
}

@override
Map<String, dynamic>? toJson(Color? color) {
return color != null ? {'a': color.a, 'r': color.r, 'g': color.g, 'b': color.b} : null;
}
}

extension UciExtension on Pick {
/// Matches a UciCharPair from a string.
UciCharPair asUciCharPairOrThrow() {
Expand Down
6 changes: 1 addition & 5 deletions lib/src/view/settings/account_preferences_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,7 @@ class _AccountPreferencesScreenState extends ConsumerState<AccountPreferencesScr
),
SwitchSettingTile(
title: Text(context.l10n.preferencesShowPlayerRatings),
subtitle: Text(
context.l10n.preferencesExplainShowPlayerRatings,
maxLines: 5,
textAlign: TextAlign.justify,
),
subtitle: Text(context.l10n.preferencesExplainShowPlayerRatings, maxLines: 5),
value: data.showRatings.value,
onChanged:
isLoading
Expand Down
2 changes: 1 addition & 1 deletion lib/src/view/settings/app_background_mode_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class _Body extends ConsumerWidget {

void onChanged(BackgroundThemeMode? value) => ref
.read(generalPreferencesProvider.notifier)
.setThemeMode(value ?? BackgroundThemeMode.system);
.setBackgroundThemeMode(value ?? BackgroundThemeMode.system);

return SafeArea(
child: ListView(
Expand Down
Loading

0 comments on commit bc90c63

Please sign in to comment.