From 1abbd8561454db808af0eee8360e1111ce721c66 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 4 Oct 2025 22:12:39 +0800 Subject: [PATCH] :sparkles: Fully customizable color scheme --- lib/main.dart | 4 +- lib/pods/config.dart | 43 +++++ lib/pods/config.freezed.dart | 343 ++++++++++++++++++++++++++++++++--- lib/pods/config.g.dart | 25 ++- lib/pods/theme.dart | 23 ++- lib/screens/settings.dart | 317 ++++++++++++++++++++++++++------ 6 files changed, 672 insertions(+), 83 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b521658f..73b18f60 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -279,8 +279,8 @@ class IslandApp extends HookConsumerWidget { return MaterialApp.router( color: Colors.transparent, - theme: theme?.light, - darkTheme: theme?.dark, + theme: theme.light, + darkTheme: theme.dark, themeMode: getThemeMode(), routerConfig: router, supportedLocales: context.supportedLocales, diff --git a/lib/pods/config.dart b/lib/pods/config.dart index 68dced96..34402c54 100644 --- a/lib/pods/config.dart +++ b/lib/pods/config.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -17,6 +19,7 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppBackgroundStoreKey = 'app_has_background'; const kAppShowBackgroundImage = 'app_show_background_image'; const kAppColorSchemeStoreKey = 'app_color_scheme'; +const kAppCustomColorsStoreKey = 'app_custom_colors'; const kAppNotifyWithHaptic = 'app_notify_with_haptic'; const kAppCustomFonts = 'app_custom_fonts'; const kAppAutoTranslate = 'app_auto_translate'; @@ -58,6 +61,21 @@ final serverUrlProvider = Provider((ref) { return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; }); +@freezed +sealed class ThemeColors with _$ThemeColors { + factory ThemeColors({ + int? primary, + int? secondary, + int? tertiary, + int? surface, + int? background, + int? error, + }) = _ThemeColors; + + factory ThemeColors.fromJson(Map json) => + _$ThemeColorsFromJson(json); +} + @freezed sealed class AppSettings with _$AppSettings { const factory AppSettings({ @@ -70,6 +88,7 @@ sealed class AppSettings with _$AppSettings { required bool showBackgroundImage, required String? customFonts, required int? appColorScheme, // The color stored via the int type + required ThemeColors? customColors, required Size? windowSize, // The window size for desktop platforms required double windowOpacity, // The window opacity for desktop platforms required double cardTransparency, // The card background opacity @@ -95,6 +114,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { showBackgroundImage: prefs.getBool(kAppShowBackgroundImage) ?? true, customFonts: prefs.getString(kAppCustomFonts), appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), + customColors: _getThemeColorsFromPrefs(prefs), windowSize: _getWindowSizeFromPrefs(prefs), windowOpacity: prefs.getDouble(kAppWindowOpacity) ?? 1.0, cardTransparency: prefs.getDouble(kAppCardTransparent) ?? 1.0, @@ -122,6 +142,18 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { return null; } + ThemeColors? _getThemeColorsFromPrefs(SharedPreferences prefs) { + final jsonString = prefs.getString(kAppCustomColorsStoreKey); + if (jsonString == null) return null; + + try { + final json = jsonDecode(jsonString); + return ThemeColors.fromJson(json); + } catch (e) { + return null; + } + } + void setDefaultPoolId(String? value) { final prefs = ref.read(sharedPreferencesProvider); if (value != null) { @@ -230,6 +262,17 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { prefs.setBool(kMaterialYouToggleStoreKey, value); state = state.copyWith(useMaterial3: value); } + + void setCustomColors(ThemeColors? value) { + final prefs = ref.read(sharedPreferencesProvider); + if (value != null) { + final json = jsonEncode(value.toJson()); + prefs.setString(kAppCustomColorsStoreKey, json); + } else { + prefs.remove(kAppCustomColorsStoreKey); + } + state = state.copyWith(customColors: value); + } } final updateInfoProvider = diff --git a/lib/pods/config.freezed.dart b/lib/pods/config.freezed.dart index 7c0c30e6..b26ad168 100644 --- a/lib/pods/config.freezed.dart +++ b/lib/pods/config.freezed.dart @@ -11,11 +11,283 @@ part of 'config.dart'; // dart format off T _$identity(T value) => value; + +/// @nodoc +mixin _$ThemeColors { + + int? get primary; int? get secondary; int? get tertiary; int? get surface; int? get background; int? get error; +/// Create a copy of ThemeColors +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ThemeColorsCopyWith get copyWith => _$ThemeColorsCopyWithImpl(this as ThemeColors, _$identity); + + /// Serializes this ThemeColors to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ThemeColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.secondary, secondary) || other.secondary == secondary)&&(identical(other.tertiary, tertiary) || other.tertiary == tertiary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.background, background) || other.background == background)&&(identical(other.error, error) || other.error == error)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,primary,secondary,tertiary,surface,background,error); + +@override +String toString() { + return 'ThemeColors(primary: $primary, secondary: $secondary, tertiary: $tertiary, surface: $surface, background: $background, error: $error)'; +} + + +} + +/// @nodoc +abstract mixin class $ThemeColorsCopyWith<$Res> { + factory $ThemeColorsCopyWith(ThemeColors value, $Res Function(ThemeColors) _then) = _$ThemeColorsCopyWithImpl; +@useResult +$Res call({ + int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error +}); + + + + +} +/// @nodoc +class _$ThemeColorsCopyWithImpl<$Res> + implements $ThemeColorsCopyWith<$Res> { + _$ThemeColorsCopyWithImpl(this._self, this._then); + + final ThemeColors _self; + final $Res Function(ThemeColors) _then; + +/// Create a copy of ThemeColors +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? primary = freezed,Object? secondary = freezed,Object? tertiary = freezed,Object? surface = freezed,Object? background = freezed,Object? error = freezed,}) { + return _then(_self.copyWith( +primary: freezed == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable +as int?,secondary: freezed == secondary ? _self.secondary : secondary // ignore: cast_nullable_to_non_nullable +as int?,tertiary: freezed == tertiary ? _self.tertiary : tertiary // ignore: cast_nullable_to_non_nullable +as int?,surface: freezed == surface ? _self.surface : surface // ignore: cast_nullable_to_non_nullable +as int?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable +as int?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ThemeColors]. +extension ThemeColorsPatterns on ThemeColors { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ThemeColors value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ThemeColors() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ThemeColors value) $default,){ +final _that = this; +switch (_that) { +case _ThemeColors(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ThemeColors value)? $default,){ +final _that = this; +switch (_that) { +case _ThemeColors() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ThemeColors() when $default != null: +return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error) $default,) {final _that = this; +switch (_that) { +case _ThemeColors(): +return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error)? $default,) {final _that = this; +switch (_that) { +case _ThemeColors() when $default != null: +return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ThemeColors implements ThemeColors { + _ThemeColors({this.primary, this.secondary, this.tertiary, this.surface, this.background, this.error}); + factory _ThemeColors.fromJson(Map json) => _$ThemeColorsFromJson(json); + +@override final int? primary; +@override final int? secondary; +@override final int? tertiary; +@override final int? surface; +@override final int? background; +@override final int? error; + +/// Create a copy of ThemeColors +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ThemeColorsCopyWith<_ThemeColors> get copyWith => __$ThemeColorsCopyWithImpl<_ThemeColors>(this, _$identity); + +@override +Map toJson() { + return _$ThemeColorsToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ThemeColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.secondary, secondary) || other.secondary == secondary)&&(identical(other.tertiary, tertiary) || other.tertiary == tertiary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.background, background) || other.background == background)&&(identical(other.error, error) || other.error == error)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,primary,secondary,tertiary,surface,background,error); + +@override +String toString() { + return 'ThemeColors(primary: $primary, secondary: $secondary, tertiary: $tertiary, surface: $surface, background: $background, error: $error)'; +} + + +} + +/// @nodoc +abstract mixin class _$ThemeColorsCopyWith<$Res> implements $ThemeColorsCopyWith<$Res> { + factory _$ThemeColorsCopyWith(_ThemeColors value, $Res Function(_ThemeColors) _then) = __$ThemeColorsCopyWithImpl; +@override @useResult +$Res call({ + int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error +}); + + + + +} +/// @nodoc +class __$ThemeColorsCopyWithImpl<$Res> + implements _$ThemeColorsCopyWith<$Res> { + __$ThemeColorsCopyWithImpl(this._self, this._then); + + final _ThemeColors _self; + final $Res Function(_ThemeColors) _then; + +/// Create a copy of ThemeColors +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? primary = freezed,Object? secondary = freezed,Object? tertiary = freezed,Object? surface = freezed,Object? background = freezed,Object? error = freezed,}) { + return _then(_ThemeColors( +primary: freezed == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable +as int?,secondary: freezed == secondary ? _self.secondary : secondary // ignore: cast_nullable_to_non_nullable +as int?,tertiary: freezed == tertiary ? _self.tertiary : tertiary // ignore: cast_nullable_to_non_nullable +as int?,surface: freezed == surface ? _self.surface : surface // ignore: cast_nullable_to_non_nullable +as int?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable +as int?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + + +} + /// @nodoc mixin _$AppSettings { bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type - Size? get windowSize;// The window size for desktop platforms + ThemeColors? get customColors; Size? get windowSize;// The window size for desktop platforms double get windowOpacity;// The window opacity for desktop platforms double get cardTransparency;// The card background opacity String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode; bool get useMaterial3; @@ -29,16 +301,16 @@ $AppSettingsCopyWith get copyWith => _$AppSettingsCopyWithImpl Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3); +int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3); @override String toString() { - return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3)'; + return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3)'; } @@ -49,11 +321,11 @@ abstract mixin class $AppSettingsCopyWith<$Res> { factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; @useResult $Res call({ - bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3 + bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3 }); - +$ThemeColorsCopyWith<$Res>? get customColors; } /// @nodoc @@ -66,7 +338,7 @@ class _$AppSettingsCopyWithImpl<$Res> /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,}) { return _then(_self.copyWith( autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable @@ -77,7 +349,8 @@ as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable -as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable +as int?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable +as ThemeColors?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable as double,cardTransparency: null == cardTransparency ? _self.cardTransparency : cardTransparency // ignore: cast_nullable_to_non_nullable as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable @@ -87,7 +360,19 @@ as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial as bool, )); } +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ThemeColorsCopyWith<$Res>? get customColors { + if (_self.customColors == null) { + return null; + } + return $ThemeColorsCopyWith<$Res>(_self.customColors!, (value) { + return _then(_self.copyWith(customColors: value)); + }); +} } @@ -166,10 +451,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _AppSettings() when $default != null: -return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);case _: +return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);case _: return orElse(); } @@ -187,10 +472,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3) $default,) {final _that = this; switch (_that) { case _AppSettings(): -return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);} +return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);} } /// A variant of `when` that fallback to returning `null` /// @@ -204,10 +489,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3)? $default,) {final _that = this; switch (_that) { case _AppSettings() when $default != null: -return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);case _: +return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);case _: return null; } @@ -219,7 +504,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha class _AppSettings implements AppSettings { - const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.useMaterial3}); + const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.customColors, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.useMaterial3}); @override final bool autoTranslate; @@ -232,6 +517,7 @@ class _AppSettings implements AppSettings { @override final String? customFonts; @override final int? appColorScheme; // The color stored via the int type +@override final ThemeColors? customColors; @override final Size? windowSize; // The window size for desktop platforms @override final double windowOpacity; @@ -253,16 +539,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_ @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)); } @override -int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3); +int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3); @override String toString() { - return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3)'; + return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3)'; } @@ -273,11 +559,11 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; @override @useResult $Res call({ - bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3 + bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3 }); - +@override $ThemeColorsCopyWith<$Res>? get customColors; } /// @nodoc @@ -290,7 +576,7 @@ class __$AppSettingsCopyWithImpl<$Res> /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,}) { return _then(_AppSettings( autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable @@ -301,7 +587,8 @@ as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable -as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable +as int?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable +as ThemeColors?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable as double,cardTransparency: null == cardTransparency ? _self.cardTransparency : cardTransparency // ignore: cast_nullable_to_non_nullable as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable @@ -312,7 +599,19 @@ as bool, )); } +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ThemeColorsCopyWith<$Res>? get customColors { + if (_self.customColors == null) { + return null; + } + return $ThemeColorsCopyWith<$Res>(_self.customColors!, (value) { + return _then(_self.copyWith(customColors: value)); + }); +} } // dart format on diff --git a/lib/pods/config.g.dart b/lib/pods/config.g.dart index 704abe48..de807629 100644 --- a/lib/pods/config.g.dart +++ b/lib/pods/config.g.dart @@ -2,12 +2,35 @@ part of 'config.dart'; +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ThemeColors _$ThemeColorsFromJson(Map json) => _ThemeColors( + primary: (json['primary'] as num?)?.toInt(), + secondary: (json['secondary'] as num?)?.toInt(), + tertiary: (json['tertiary'] as num?)?.toInt(), + surface: (json['surface'] as num?)?.toInt(), + background: (json['background'] as num?)?.toInt(), + error: (json['error'] as num?)?.toInt(), +); + +Map _$ThemeColorsToJson(_ThemeColors instance) => + { + 'primary': instance.primary, + 'secondary': instance.secondary, + 'tertiary': instance.tertiary, + 'surface': instance.surface, + 'background': instance.background, + 'error': instance.error, + }; + // ************************************************************************** // RiverpodGenerator // ************************************************************************** String _$appSettingsNotifierHash() => - r'514c771a398f4647a22b2d51ae0152bff713833f'; + r'3ba2cdce76f3c4fed84f4108341c88a0a971bf3a'; /// See also [AppSettingsNotifier]. @ProviderFor(AppSettingsNotifier) diff --git a/lib/pods/theme.dart b/lib/pods/theme.dart index 5497a3f3..2e4274b7 100644 --- a/lib/pods/theme.dart +++ b/lib/pods/theme.dart @@ -31,11 +31,32 @@ ThemeData createAppTheme(Brightness brightness, AppSettings settings) { ? Color(settings.appColorScheme!) : Colors.indigo; - final colorScheme = ColorScheme.fromSeed( + var colorScheme = ColorScheme.fromSeed( seedColor: seedColor, brightness: brightness, ); + final customColors = settings.customColors; + if (customColors != null) { + colorScheme = colorScheme.copyWith( + primary: + customColors.primary != null ? Color(customColors.primary!) : null, + secondary: + customColors.secondary != null + ? Color(customColors.secondary!) + : null, + tertiary: + customColors.tertiary != null ? Color(customColors.tertiary!) : null, + surface: + customColors.surface != null ? Color(customColors.surface!) : null, + background: + customColors.background != null + ? Color(customColors.background!) + : null, + error: customColors.error != null ? Color(customColors.error!) : null, + ); + } + final hasAppBarTransparent = settings.appBarTransparent; final useM3 = settings.useMaterial3; diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 8c1c5757..102c8705 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -234,68 +234,191 @@ class SettingsScreen extends HookConsumerWidget { ), // Color scheme settings - ListTile( - minLeadingWidth: 48, - title: Text('settingsColorScheme').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.palette), - trailing: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) { - Color selectedColor = - settings.appColorScheme != null - ? Color(settings.appColorScheme!) - : Colors.indigo; + Theme( + data: Theme.of( + context, + ).copyWith(listTileTheme: ListTileThemeData(minLeadingWidth: 48)), + child: ExpansionTile( + title: Text('settingsColorScheme').tr(), + tilePadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.palette), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Seed color picker + ListTile( + title: Text('Seed Color').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + trailing: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) { + Color selectedColor = + settings.appColorScheme != null + ? Color(settings.appColorScheme!) + : Colors.indigo; - return AlertDialog( - title: Text('settingsColorScheme').tr(), - content: SingleChildScrollView( - child: ColorPicker( - paletteType: PaletteType.rgbWithBlue, - enableAlpha: false, - pickerColor: selectedColor, - onColorChanged: (color) { - selectedColor = color; - }, + return AlertDialog( + title: Text('Seed Color').tr(), + content: SingleChildScrollView( + child: ColorPicker( + paletteType: PaletteType.hsv, + enableAlpha: true, + showLabel: true, + hexInputBar: true, + pickerColor: selectedColor, + onColorChanged: (color) { + selectedColor = color; + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('cancel').tr(), + ), + TextButton( + onPressed: () { + ref + .read(appSettingsNotifierProvider.notifier) + .setAppColorScheme(selectedColor.value); + Navigator.of(context).pop(); + }, + child: Text('confirm').tr(), + ), + ], + ); + }, + ); + }, + child: Container( + width: 24, + height: 24, + margin: EdgeInsets.symmetric(horizontal: 2, vertical: 8), + decoration: BoxDecoration( + color: + settings.appColorScheme != null + ? Color(settings.appColorScheme!) + : Colors.indigo, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.5), + width: 2, ), ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('cancel').tr(), - ), - TextButton( - onPressed: () { - ref - .read(appSettingsNotifierProvider.notifier) - .setAppColorScheme(selectedColor.value); - Navigator.of(context).pop(); - }, - child: Text('confirm').tr(), - ), - ], - ); - }, - ); - }, - child: Container( - width: 24, - height: 24, - margin: EdgeInsets.symmetric(horizontal: 2, vertical: 8), - decoration: BoxDecoration( - color: - settings.appColorScheme != null - ? Color(settings.appColorScheme!) - : Colors.indigo, - shape: BoxShape.circle, - border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.5), - width: 2, + ), ), ), - ), + // Custom colors section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: + Text( + 'Custom Colors', + style: Theme.of(context).textTheme.titleMedium, + ).bold(), + ), + // Primary color + _ColorPickerTile( + title: 'Primary', + color: + settings.customColors?.primary != null + ? Color(settings.customColors!.primary!) + : null, + onColorChanged: (color) { + final current = settings.customColors ?? ThemeColors(); + ref + .read(appSettingsNotifierProvider.notifier) + .setCustomColors(current.copyWith(primary: color?.value)); + }, + ), + // Secondary + _ColorPickerTile( + title: 'Secondary', + color: + settings.customColors?.secondary != null + ? Color(settings.customColors!.secondary!) + : null, + onColorChanged: (color) { + final current = settings.customColors ?? ThemeColors(); + ref + .read(appSettingsNotifierProvider.notifier) + .setCustomColors(current.copyWith(secondary: color?.value)); + }, + ), + // Tertiary + _ColorPickerTile( + title: 'Tertiary', + color: + settings.customColors?.tertiary != null + ? Color(settings.customColors!.tertiary!) + : null, + onColorChanged: (color) { + final current = settings.customColors ?? ThemeColors(); + ref + .read(appSettingsNotifierProvider.notifier) + .setCustomColors(current.copyWith(tertiary: color?.value)); + }, + ), + // Surface + _ColorPickerTile( + title: 'Surface', + color: + settings.customColors?.surface != null + ? Color(settings.customColors!.surface!) + : null, + onColorChanged: (color) { + final current = settings.customColors ?? ThemeColors(); + ref + .read(appSettingsNotifierProvider.notifier) + .setCustomColors(current.copyWith(surface: color?.value)); + }, + ), + // Background + _ColorPickerTile( + title: 'Background', + color: + settings.customColors?.background != null + ? Color(settings.customColors!.background!) + : null, + onColorChanged: (color) { + final current = settings.customColors ?? ThemeColors(); + ref + .read(appSettingsNotifierProvider.notifier) + .setCustomColors( + current.copyWith(background: color?.value), + ); + }, + ), + // Error + _ColorPickerTile( + title: 'Error', + color: + settings.customColors?.error != null + ? Color(settings.customColors!.error!) + : null, + onColorChanged: (color) { + final current = settings.customColors ?? ThemeColors(); + ref + .read(appSettingsNotifierProvider.notifier) + .setCustomColors(current.copyWith(error: color?.value)); + }, + ), + // Reset custom colors + ListTile( + title: Text('Reset Custom Colors').tr(), + trailing: const Icon(Symbols.restart_alt).padding(right: 2), + contentPadding: EdgeInsets.symmetric(horizontal: 20), + onTap: () { + ref + .read(appSettingsNotifierProvider.notifier) + .setCustomColors(null); + showSnackBar('settingsApplied'.tr()); + }, + ), + ], ), ), @@ -811,3 +934,83 @@ class _SettingsSection extends StatelessWidget { ); } } + +// Helper widget for color picker tiles +class _ColorPickerTile extends StatelessWidget { + final String title; + final Color? color; + final ValueChanged onColorChanged; + + const _ColorPickerTile({ + required this.title, + required this.color, + required this.onColorChanged, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + trailing: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) { + Color selectedColor = color ?? Colors.transparent; + + return AlertDialog( + title: Text(title), + content: SingleChildScrollView( + child: ColorPicker( + paletteType: PaletteType.hsv, + enableAlpha: true, + showLabel: true, + hexInputBar: true, + pickerColor: selectedColor, + onColorChanged: (newColor) { + selectedColor = newColor; + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('cancel').tr(), + ), + TextButton( + onPressed: () { + onColorChanged(selectedColor); + Navigator.of(context).pop(); + }, + child: Text('confirm').tr(), + ), + TextButton( + onPressed: () { + onColorChanged(null); + Navigator.of(context).pop(); + }, + child: Text('Reset').tr(), + ), + ], + ); + }, + ); + }, + child: Container( + width: 24, + height: 24, + margin: EdgeInsets.symmetric(horizontal: 2, vertical: 8), + decoration: BoxDecoration( + color: color ?? Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + width: 2, + ), + ), + ), + ), + ); + } +}