feat(ui): add pointer cursors to clickable controls

close #1068
This commit is contained in:
edde746
2026-05-18 15:56:41 +02:00
parent 324d145292
commit 5a68711f7b
24 changed files with 772 additions and 649 deletions
+105 -91
View File
@@ -6,6 +6,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../screens/settings/settings_utils.dart';
import '../services/settings_service.dart';
import 'app_icon.dart';
import 'clickable_cursor.dart';
import 'settings_section.dart';
/// Reactive setting tiles bound to a [Pref] via [SettingsService.listenable].
@@ -42,19 +43,22 @@ class SettingSwitchTile extends StatelessWidget {
final svc = _TileBase._svc;
return ValueListenableBuilder<bool>(
valueListenable: svc.listenable(pref),
builder: (_, value, _) => SwitchListTile(
focusNode: focusNode,
secondary: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
value: value,
onChanged: enabled
? (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
}
: null,
builder: (_, value, _) => ClickableCursor(
enabled: enabled,
child: SwitchListTile(
focusNode: focusNode,
secondary: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
value: value,
onChanged: enabled
? (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
}
: null,
),
),
);
}
@@ -83,13 +87,15 @@ class SettingNavigationTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
focusNode: focusNode,
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: AppIcon(trailingIcon, fill: 1),
onTap: onTap ?? () => Navigator.push(context, MaterialPageRoute(builder: destinationBuilder!)),
return ClickableCursor(
child: ListTile(
focusNode: focusNode,
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: AppIcon(trailingIcon, fill: 1),
onTap: onTap ?? () => Navigator.push(context, MaterialPageRoute(builder: destinationBuilder!)),
),
);
}
}
@@ -124,24 +130,26 @@ class SettingNumberTile extends StatelessWidget {
final svc = _TileBase._svc;
return ValueListenableBuilder<int>(
valueListenable: svc.listenable(pref),
builder: (_, value, _) => ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: Text(subtitleBuilder(value)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: title,
labelText: labelText,
suffixText: suffixText,
min: min,
max: max,
currentValue: value,
onSave: (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
},
builder: (_, value, _) => ClickableCursor(
child: ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: Text(subtitleBuilder(value)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: title,
labelText: labelText,
suffixText: suffixText,
min: min,
max: max,
currentValue: value,
onSave: (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
},
),
),
),
);
@@ -180,23 +188,25 @@ class SettingSelectionTile<T, S> extends StatelessWidget {
valueListenable: svc.listenable(pref),
builder: (_, raw, _) {
final value = decode(raw);
return ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: Text(subtitleBuilder(value)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () async {
final picked = await showSelectionDialog<T>(
context: context,
title: title,
options: options,
currentValue: value,
);
if (picked == null) return;
await svc.write(pref, encode(picked));
final callback = onAfterWrite;
if (callback != null) await callback(picked);
},
return ClickableCursor(
child: ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: Text(subtitleBuilder(value)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () async {
final picked = await showSelectionDialog<T>(
context: context,
title: title,
options: options,
currentValue: value,
);
if (picked == null) return;
await svc.write(pref, encode(picked));
final callback = onAfterWrite;
if (callback != null) await callback(picked);
},
),
);
},
);
@@ -227,21 +237,23 @@ class SettingRegexTile extends StatelessWidget {
final svc = _TileBase._svc;
return ValueListenableBuilder<String>(
valueListenable: svc.listenable(pref),
builder: (_, value, _) => ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: Text(subtitle),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showRegexInputDialog(
context: context,
title: title,
currentValue: value,
defaultValue: defaultValue,
onSave: (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
},
builder: (_, value, _) => ClickableCursor(
child: ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: Text(subtitle),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showRegexInputDialog(
context: context,
title: title,
currentValue: value,
defaultValue: defaultValue,
onSave: (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
},
),
),
),
);
@@ -317,28 +329,30 @@ class SettingColorTile extends StatelessWidget {
final svc = _TileBase._svc;
return ValueListenableBuilder<String>(
valueListenable: svc.listenable(pref),
builder: (_, hex, _) => ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: hexToColor(hex),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
builder: (_, hex, _) => ClickableCursor(
child: ListTile(
leading: AppIcon(icon, fill: 1),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: hexToColor(hex),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
),
),
onTap: () => showColorInputDialog(
context: context,
title: title,
currentHex: hex,
onSave: (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
},
),
),
onTap: () => showColorInputDialog(
context: context,
title: title,
currentHex: hex,
onSave: (v) async {
await svc.write(pref, v);
final callback = onAfterWrite;
if (callback != null) await callback(v);
},
),
),
);