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,
"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",
"type": "rectangle",
"x": 561.2432261444915,
"y": 505.261726567147,
"width": 146.7306357461261,
"height": 40.63309912969646,
"y": 509.59787769019385,
"width": 123.56657324612611,
"height": 36.296948006649586,
"angle": 0,
"strokeColor": "#1f2937",
"backgroundColor": "#ffffff",
@ -487,11 +418,11 @@
"index": "aD",
"roundness": null,
"seed": 1608525080,
"version": 101,
"versionNonce": 679299830,
"version": 114,
"versionNonce": 1580272529,
"isDeleted": false,
"boundElements": [],
"updated": 1756647235527,
"updated": 1758364887319,
"link": null,
"locked": false
},
@ -500,7 +431,7 @@
"type": "text",
"x": 601.8763252741879,
"y": 514.291304151524,
"width": 39.54961113185798,
"width": 45.983367919921875,
"height": 24.379859477817877,
"angle": 0,
"strokeColor": "#1f2937",
@ -517,20 +448,20 @@
"index": "aE",
"roundness": null,
"seed": 533447192,
"version": 103,
"versionNonce": 554272618,
"version": 111,
"versionNonce": 935775633,
"isDeleted": false,
"boundElements": [],
"updated": 1756647235527,
"updated": 1758364882876,
"link": null,
"locked": false,
"text": "Save",
"text": "Close",
"fontSize": 18.059155168753982,
"fontFamily": 6,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Save",
"originalText": "Close",
"autoResize": true,
"lineHeight": 1.35
},

View File

@ -32,7 +32,8 @@ Route: root --> settings
Design notes:
- Opened via "Configure" button in the right of top bar.
- 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:

View File

@ -108,15 +108,37 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
// (useful if some code persisted mat.toString()).
for (final mc in Colors.primaries) {
if (mc.toString() == v) {
return Color(mc.value);
return mc; // MaterialColor extends Color
}
}
return null;
}
static String _toHex(Color c) =>
'#${c.value.toRadixString(16).padLeft(8, '0').toUpperCase()}';
static String _toHex(Color c) {
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)
: super(
PreferencesState(

View File

@ -12,7 +12,6 @@ class SettingsDialog extends ConsumerStatefulWidget {
class _SettingsDialogState extends ConsumerState<SettingsDialog> {
String? _theme;
String? _themeColor;
String? _language;
// Page view removed; continuous-only
double? _exportDpi;
@ -22,7 +21,6 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
super.initState();
final prefs = ref.read(preferencesRepositoryProvider);
_theme = prefs.theme;
_themeColor = prefs.theme_color;
_language = prefs.language;
_exportDpi = prefs.exportDpi;
// pageView no longer configurable (continuous-only)
@ -74,8 +72,8 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
child: CircularProgressIndicator(),
),
),
error: (_, _) {
final items =
error: (_, __) {
final tags =
AppLocalizations.supportedLocales
.map((loc) => toLanguageTag(loc))
.toList()
@ -85,19 +83,27 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
isExpanded: true,
value: _language,
items:
items
tags
.map(
(tag) => DropdownMenuItem(
(tag) => DropdownMenuItem<String>(
value: tag,
child: Text(tag),
),
)
.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) {
final items =
final tags =
AppLocalizations.supportedLocales
.map((loc) => toLanguageTag(loc))
.toList()
@ -107,7 +113,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
isExpanded: true,
value: _language,
items:
items
tags
.map(
(tag) => DropdownMenuItem<String>(
value: tag,
@ -115,7 +121,15 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
),
)
.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(),
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),
),
],
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
.read(preferencesRepositoryProvider.notifier)
.setThemeColor(value);
setState(() => _themeColor = value);
},
),
],
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () => Navigator.of(context).pop(false),
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),
),
],
Align(
alignment: Alignment.centerRight,
child: FilledButton.tonal(
onPressed: () => Navigator.of(context).pop(true),
child: Text(l.close),
),
),
],
),
@ -282,26 +289,49 @@ class _ThemeColorPickerDialog extends StatelessWidget {
child: Wrap(
spacing: 12,
runSpacing: 12,
children: Colors.primaries.map((mat) {
final c = Color(mat.value);
final selected = c.value == currentColor.value;
// Store as ARGB hex string, e.g., #FF2196F3
String hex(Color color) =>
'#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}';
return InkWell(
key: Key('pick_${mat.value}'),
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(),
children:
Colors.primaries.map((mat) {
final Color c = mat; // MaterialColor is a Color
final selected = c == currentColor;
// Store as ARGB hex string, e.g., #FF2196F3
String hex(Color color) {
final a =
((color.a * 255.0).round() & 0xff)
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
final r =
((color.r * 255.0).round() & 0xff)
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
final g =
((color.g * 255.0).round() & 0xff)
.toRadixString(16)
.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: [