import 'dart:async'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../screens/settings/settings_utils.dart'; import '../services/settings_service.dart'; import '../utils/platform_detector.dart'; import 'app_icon.dart'; import 'focusable_list_tile.dart'; import 'settings_section.dart'; /// Reactive setting tiles bound to a [Pref] via [SettingsService.listenable]. /// Eliminates the field-mirror + setState + manual reload pattern that used to /// surround every settings row. class _TileBase { static SettingsService get _svc => SettingsService.instance; } /// SwitchListTile bound to a [Pref]. class SettingSwitchTile extends StatelessWidget { final Pref pref; final IconData icon; final String title; final String? subtitle; final FutureOr Function(bool)? onAfterWrite; final bool enabled; final FocusNode? focusNode; const SettingSwitchTile({ super.key, required this.pref, required this.icon, required this.title, this.subtitle, this.onAfterWrite, this.enabled = true, this.focusNode, }); @override Widget build(BuildContext context) { final isMobile = PlatformDetector.isMobile(context); final svc = _TileBase._svc; return ValueListenableBuilder( valueListenable: svc.listenable(pref), builder: (_, value, _) => FocusableSwitchListTile( focusNode: focusNode, secondary: AppIcon(icon, fill: 1), title: Text(title, style: settingsOptionTitleStyle(context)), subtitle: subtitle != null ? Text(subtitle!) : null, value: value, dense: isMobile, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard, onChanged: enabled ? (v) async { await svc.write(pref, v); final callback = onAfterWrite; if (callback != null) await callback(v); } : null, ), ); } } /// Standard settings row that navigates to another screen. class SettingNavigationTile extends StatelessWidget { final IconData icon; final String title; final String? subtitle; final WidgetBuilder? destinationBuilder; final VoidCallback? onTap; final FocusNode? focusNode; final IconData trailingIcon; const SettingNavigationTile({ super.key, required this.icon, required this.title, this.subtitle, this.destinationBuilder, this.onTap, this.focusNode, this.trailingIcon = Symbols.chevron_right_rounded, }) : assert(destinationBuilder != null || onTap != null); @override Widget build(BuildContext context) { final isMobile = PlatformDetector.isMobile(context); return FocusableListTile( focusNode: focusNode, leading: AppIcon(icon, fill: 1), title: Text(title, style: settingsOptionTitleStyle(context)), subtitle: subtitle != null ? Text(subtitle!) : null, trailing: AppIcon(trailingIcon, fill: 1), onTap: onTap ?? () => Navigator.push(context, MaterialPageRoute(builder: destinationBuilder!)), dense: isMobile, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard, ); } } /// ListTile that opens [showNumericInputDialog] and writes back. class SettingNumberTile extends StatelessWidget { final Pref pref; final IconData icon; final String title; final String Function(int) subtitleBuilder; final String labelText; final String suffixText; final int min; final int max; final FutureOr Function(int)? onAfterWrite; const SettingNumberTile({ super.key, required this.pref, required this.icon, required this.title, required this.subtitleBuilder, required this.labelText, required this.suffixText, required this.min, required this.max, this.onAfterWrite, }); @override Widget build(BuildContext context) { final isMobile = PlatformDetector.isMobile(context); final svc = _TileBase._svc; return ValueListenableBuilder( valueListenable: svc.listenable(pref), builder: (_, value, _) => FocusableListTile( leading: AppIcon(icon, fill: 1), title: Text(title, style: settingsOptionTitleStyle(context)), subtitle: Text(subtitleBuilder(value)), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), dense: isMobile, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard, 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); }, ), ), ); } } /// ListTile that opens [showSelectionDialog] and writes the chosen value. /// [encode]/[decode] map between the [Pref] storage type and the option /// type [T] (e.g. enum-stored-as-string preset → [TranscodeQualityPreset]). class SettingSelectionTile extends StatelessWidget { final Pref pref; final IconData icon; final String title; final String Function(T) subtitleBuilder; final List> options; final T Function(S) decode; final S Function(T) encode; final FutureOr Function(T)? onAfterWrite; const SettingSelectionTile({ super.key, required this.pref, required this.icon, required this.title, required this.subtitleBuilder, required this.options, required this.decode, required this.encode, this.onAfterWrite, }); @override Widget build(BuildContext context) { final isMobile = PlatformDetector.isMobile(context); final svc = _TileBase._svc; return ValueListenableBuilder( valueListenable: svc.listenable(pref), builder: (_, raw, _) { final value = decode(raw); return FocusableListTile( leading: AppIcon(icon, fill: 1), title: Text(title, style: settingsOptionTitleStyle(context)), subtitle: Text(subtitleBuilder(value)), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), dense: isMobile, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard, onTap: () async { final picked = await showSelectionDialog( 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); }, ); }, ); } } /// ListTile that opens [showRegexInputDialog] for a [Pref]. class SettingRegexTile extends StatelessWidget { final Pref pref; final IconData icon; final String title; final String subtitle; final String defaultValue; final FutureOr Function(String)? onAfterWrite; const SettingRegexTile({ super.key, required this.pref, required this.icon, required this.title, required this.subtitle, required this.defaultValue, this.onAfterWrite, }); @override Widget build(BuildContext context) { final isMobile = PlatformDetector.isMobile(context); final svc = _TileBase._svc; return ValueListenableBuilder( valueListenable: svc.listenable(pref), builder: (_, value, _) => FocusableListTile( leading: AppIcon(icon, fill: 1), title: Text(title, style: settingsOptionTitleStyle(context)), subtitle: Text(subtitle), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), dense: isMobile, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard, 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); }, ), ), ); } } /// SegmentedSetting bound to a [Pref]. Use [encode]/[decode] when the /// stored type differs from the segment type (e.g. bool stored, segments /// over enum). class SettingSegmentedTile extends StatelessWidget { final Pref pref; final IconData icon; final String title; final List> segments; final T Function(S) decode; final S Function(T) encode; final FutureOr Function(T)? onAfterWrite; const SettingSegmentedTile({ super.key, required this.pref, required this.icon, required this.title, required this.segments, required this.decode, required this.encode, this.onAfterWrite, }); @override Widget build(BuildContext context) { final svc = _TileBase._svc; return ValueListenableBuilder( valueListenable: svc.listenable(pref), builder: (_, raw, _) { final value = decode(raw); return SegmentedSetting( icon: icon, title: title, segments: segments, selected: value, onChanged: (v) async { await svc.write(pref, encode(v)); final callback = onAfterWrite; if (callback != null) await callback(v); }, ); }, ); } } /// ListTile that opens [showColorInputDialog] for a hex-string [Pref]. /// Trailing widget is a small color swatch. class SettingColorTile extends StatelessWidget { final Pref pref; final IconData icon; final String title; final String? subtitle; final FutureOr Function(String hex)? onAfterWrite; const SettingColorTile({ super.key, required this.pref, required this.icon, required this.title, this.subtitle, this.onAfterWrite, }); @override Widget build(BuildContext context) { final isMobile = PlatformDetector.isMobile(context); final svc = _TileBase._svc; return ValueListenableBuilder( valueListenable: svc.listenable(pref), builder: (_, hex, _) => FocusableListTile( leading: AppIcon(icon, fill: 1), title: Text(title, style: settingsOptionTitleStyle(context)), 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), ), ), dense: isMobile, visualDensity: isMobile ? const VisualDensity(vertical: -3) : VisualDensity.standard, 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); }, ), ), ); } }