feat: add theme color selection in setting dialog

This commit is contained in:
insleker 2025-09-20 19:06:10 +08:00
parent 8197a352aa
commit 7032f22327
4 changed files with 126 additions and 142 deletions

View File

@ -396,82 +396,13 @@
"link": null, "link": null,
"locked": false "locked": false
}, },
{
"id": "Q0v5ejctIV2msui0iDFEg",
"type": "rectangle",
"x": 414.5125903983653,
"y": 505.261726567147,
"width": 124.15669178518363,
"height": 40.63309912969646,
"angle": 0,
"strokeColor": "#1f2937",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [
"nQmqS53zA9IffPy8AAZwV"
],
"frameId": null,
"index": "aB",
"roundness": null,
"seed": 625347352,
"version": 101,
"versionNonce": 1373172150,
"isDeleted": false,
"boundElements": [],
"updated": 1756647235527,
"link": null,
"locked": false
},
{
"id": "QSD6mQUNvCKRLZtin0AHX",
"type": "text",
"x": 442.73002034954345,
"y": 514.291304151524,
"width": 55.13471219456543,
"height": 24.379859477817877,
"angle": 0,
"strokeColor": "#1f2937",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [
"nQmqS53zA9IffPy8AAZwV"
],
"frameId": null,
"index": "aC",
"roundness": null,
"seed": 1267001368,
"version": 103,
"versionNonce": 162573482,
"isDeleted": false,
"boundElements": [],
"updated": 1756647235527,
"link": null,
"locked": false,
"text": "Cancel",
"fontSize": 18.059155168753982,
"fontFamily": 6,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Cancel",
"autoResize": true,
"lineHeight": 1.35
},
{ {
"id": "fmP0hKBOaNa5Ge12TEwyD", "id": "fmP0hKBOaNa5Ge12TEwyD",
"type": "rectangle", "type": "rectangle",
"x": 561.2432261444915, "x": 561.2432261444915,
"y": 505.261726567147, "y": 509.59787769019385,
"width": 146.7306357461261, "width": 123.56657324612611,
"height": 40.63309912969646, "height": 36.296948006649586,
"angle": 0, "angle": 0,
"strokeColor": "#1f2937", "strokeColor": "#1f2937",
"backgroundColor": "#ffffff", "backgroundColor": "#ffffff",
@ -487,11 +418,11 @@
"index": "aD", "index": "aD",
"roundness": null, "roundness": null,
"seed": 1608525080, "seed": 1608525080,
"version": 101, "version": 114,
"versionNonce": 679299830, "versionNonce": 1580272529,
"isDeleted": false, "isDeleted": false,
"boundElements": [], "boundElements": [],
"updated": 1756647235527, "updated": 1758364887319,
"link": null, "link": null,
"locked": false "locked": false
}, },
@ -500,7 +431,7 @@
"type": "text", "type": "text",
"x": 601.8763252741879, "x": 601.8763252741879,
"y": 514.291304151524, "y": 514.291304151524,
"width": 39.54961113185798, "width": 45.983367919921875,
"height": 24.379859477817877, "height": 24.379859477817877,
"angle": 0, "angle": 0,
"strokeColor": "#1f2937", "strokeColor": "#1f2937",
@ -517,20 +448,20 @@
"index": "aE", "index": "aE",
"roundness": null, "roundness": null,
"seed": 533447192, "seed": 533447192,
"version": 103, "version": 111,
"versionNonce": 554272618, "versionNonce": 935775633,
"isDeleted": false, "isDeleted": false,
"boundElements": [], "boundElements": [],
"updated": 1756647235527, "updated": 1758364882876,
"link": null, "link": null,
"locked": false, "locked": false,
"text": "Save", "text": "Close",
"fontSize": 18.059155168753982, "fontSize": 18.059155168753982,
"fontFamily": 6, "fontFamily": 6,
"textAlign": "left", "textAlign": "left",
"verticalAlign": "top", "verticalAlign": "top",
"containerId": null, "containerId": null,
"originalText": "Save", "originalText": "Close",
"autoResize": true, "autoResize": true,
"lineHeight": 1.35 "lineHeight": 1.35
}, },

View File

@ -32,7 +32,8 @@ Route: root --> settings
Design notes: Design notes:
- Opened via "Configure" button in the right of top bar. - Opened via "Configure" button in the right of top bar.
- Model with simple sections (e.g., General, Display). - Model with simple sections (e.g., General, Display).
- Primary action to save, secondary to cancel. - When select option, option will take effect immediately.
- A button to close the dialog and return to the previous screen.
Illustration: Illustration:

View File

@ -108,15 +108,37 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
// (useful if some code persisted mat.toString()). // (useful if some code persisted mat.toString()).
for (final mc in Colors.primaries) { for (final mc in Colors.primaries) {
if (mc.toString() == v) { if (mc.toString() == v) {
return Color(mc.value); return mc; // MaterialColor extends Color
} }
} }
return null; return null;
} }
static String _toHex(Color c) => static String _toHex(Color c) {
'#${c.value.toRadixString(16).padLeft(8, '0').toUpperCase()}'; final a =
((c.a * 255.0).round() & 0xff)
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
final r =
((c.r * 255.0).round() & 0xff)
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
final g =
((c.g * 255.0).round() & 0xff)
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
final b =
((c.b * 255.0).round() & 0xff)
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return '#$a$r$g$b';
}
PreferencesStateNotifier(this.prefs) PreferencesStateNotifier(this.prefs)
: super( : super(
PreferencesState( PreferencesState(

View File

@ -12,7 +12,6 @@ class SettingsDialog extends ConsumerStatefulWidget {
class _SettingsDialogState extends ConsumerState<SettingsDialog> { class _SettingsDialogState extends ConsumerState<SettingsDialog> {
String? _theme; String? _theme;
String? _themeColor;
String? _language; String? _language;
// Page view removed; continuous-only // Page view removed; continuous-only
double? _exportDpi; double? _exportDpi;
@ -22,7 +21,6 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
super.initState(); super.initState();
final prefs = ref.read(preferencesRepositoryProvider); final prefs = ref.read(preferencesRepositoryProvider);
_theme = prefs.theme; _theme = prefs.theme;
_themeColor = prefs.theme_color;
_language = prefs.language; _language = prefs.language;
_exportDpi = prefs.exportDpi; _exportDpi = prefs.exportDpi;
// pageView no longer configurable (continuous-only) // pageView no longer configurable (continuous-only)
@ -74,8 +72,8 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
), ),
error: (_, _) { error: (_, __) {
final items = final tags =
AppLocalizations.supportedLocales AppLocalizations.supportedLocales
.map((loc) => toLanguageTag(loc)) .map((loc) => toLanguageTag(loc))
.toList() .toList()
@ -85,19 +83,27 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
isExpanded: true, isExpanded: true,
value: _language, value: _language,
items: items:
items tags
.map( .map(
(tag) => DropdownMenuItem( (tag) => DropdownMenuItem<String>(
value: tag, value: tag,
child: Text(tag), child: Text(tag),
), ),
) )
.toList(), .toList(),
onChanged: (v) => setState(() => _language = v), onChanged: (v) async {
if (v == null) return;
setState(() => _language = v);
await ref
.read(
preferencesRepositoryProvider.notifier,
)
.setLanguage(v);
},
); );
}, },
data: (names) { data: (names) {
final items = final tags =
AppLocalizations.supportedLocales AppLocalizations.supportedLocales
.map((loc) => toLanguageTag(loc)) .map((loc) => toLanguageTag(loc))
.toList() .toList()
@ -107,7 +113,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
isExpanded: true, isExpanded: true,
value: _language, value: _language,
items: items:
items tags
.map( .map(
(tag) => DropdownMenuItem<String>( (tag) => DropdownMenuItem<String>(
value: tag, value: tag,
@ -115,7 +121,15 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
), ),
) )
.toList(), .toList(),
onChanged: (v) => setState(() => _language = v), onChanged: (v) async {
if (v == null) return;
setState(() => _language = v);
await ref
.read(
preferencesRepositoryProvider.notifier,
)
.setLanguage(v);
},
); );
}, },
), ),
@ -140,7 +154,13 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
), ),
) )
.toList(), .toList(),
onChanged: (v) => setState(() => _exportDpi = v), onChanged: (v) async {
if (v == null) return;
setState(() => _exportDpi = v);
await ref
.read(preferencesRepositoryProvider.notifier)
.setExportDpi(v);
},
), ),
), ),
], ],
@ -171,7 +191,13 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
child: Text(l.themeSystem), child: Text(l.themeSystem),
), ),
], ],
onChanged: (v) => setState(() => _theme = v), onChanged: (v) async {
if (v == null) return;
setState(() => _theme = v);
await ref
.read(preferencesRepositoryProvider.notifier)
.setTheme(v);
},
), ),
), ),
], ],
@ -187,37 +213,18 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
await ref await ref
.read(preferencesRepositoryProvider.notifier) .read(preferencesRepositoryProvider.notifier)
.setThemeColor(value); .setThemeColor(value);
setState(() => _themeColor = value);
}, },
), ),
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Align(
mainAxisAlignment: MainAxisAlignment.end, alignment: Alignment.centerRight,
children: [ child: FilledButton.tonal(
OutlinedButton( onPressed: () => Navigator.of(context).pop(true),
onPressed: () => Navigator.of(context).pop(false), child: Text(l.close),
child: Text(l.cancel), ),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () async {
final n = ref.read(
preferencesRepositoryProvider.notifier,
);
if (_theme != null) await n.setTheme(_theme!);
if (_themeColor != null)
await n.setThemeColor(_themeColor!);
if (_language != null) await n.setLanguage(_language!);
if (_exportDpi != null) await n.setExportDpi(_exportDpi!);
// pageView not configurable anymore
if (mounted) Navigator.of(context).pop(true);
},
child: Text(l.save),
),
],
), ),
], ],
), ),
@ -282,26 +289,49 @@ class _ThemeColorPickerDialog extends StatelessWidget {
child: Wrap( child: Wrap(
spacing: 12, spacing: 12,
runSpacing: 12, runSpacing: 12,
children: Colors.primaries.map((mat) { children:
final c = Color(mat.value); Colors.primaries.map((mat) {
final selected = c.value == currentColor.value; final Color c = mat; // MaterialColor is a Color
// Store as ARGB hex string, e.g., #FF2196F3 final selected = c == currentColor;
String hex(Color color) => // Store as ARGB hex string, e.g., #FF2196F3
'#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}'; String hex(Color color) {
return InkWell( final a =
key: Key('pick_${mat.value}'), ((color.a * 255.0).round() & 0xff)
onTap: () => Navigator.of(context).pop(hex(c)), .toRadixString(16)
customBorder: const CircleBorder(), .padLeft(2, '0')
child: Stack( .toUpperCase();
alignment: Alignment.center, final r =
children: [ ((color.r * 255.0).round() & 0xff)
_ColorDot(color: c, size: 32), .toRadixString(16)
if (selected) .padLeft(2, '0')
const Icon(Icons.check, color: Colors.white, size: 20), .toUpperCase();
], final g =
), ((color.g * 255.0).round() & 0xff)
); .toRadixString(16)
}).toList(), .padLeft(2, '0')
.toUpperCase();
final b =
((color.b * 255.0).round() & 0xff)
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return '#$a$r$g$b';
}
return InkWell(
key: Key('pick_${hex(c)}'),
onTap: () => Navigator.of(context).pop(hex(c)),
customBorder: const CircleBorder(),
child: Stack(
alignment: Alignment.center,
children: [
_ColorDot(color: c, size: 32),
if (selected)
const Icon(Icons.check, color: Colors.white, size: 20),
],
),
);
}).toList(),
), ),
), ),
actions: [ actions: [