import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_localizations.dart'; import '../models/pen_tool.dart'; import '../models/pressure_curve.dart'; import '../providers/settings_provider.dart'; import '../services/vault_service.dart'; import '../utils/stroke_stabilizer.dart'; /// Material 3 settings screen for BadNote. class SettingsScreen extends ConsumerWidget { const SettingsScreen({super.key}); void _showColorPicker( BuildContext context, Color current, ValueChanged onPicked, ) { Color pickerColor = current; showDialog( context: context, builder: (context) { final l = AppLocalizations.of(context); return AlertDialog( title: Text(l.pickColor), content: SingleChildScrollView( child: ColorPicker( pickerColor: pickerColor, onColorChanged: (color) => pickerColor = color, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l.cancel), ), TextButton( onPressed: () { onPicked(pickerColor); Navigator.of(context).pop(); }, child: Text(l.ok), ), ], ); }, ); } void _confirmClearData(BuildContext context, WidgetRef ref) { showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(AppLocalizations.of(ctx).clearSettingsTitle), content: const Text( 'This will reset pen defaults and appearance settings. ' 'Notes and documents are not affected.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text(AppLocalizations.of(ctx).cancel), ), TextButton( onPressed: () { ref.read(settingsProvider).clearAllData(); Navigator.pop(ctx); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context).settingsReset)), ); }, child: Text(AppLocalizations.of(ctx).clear), ), ], ), ); } @override Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(settingsProvider); final colorScheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context).settings)), body: ListView( children: [ _SectionHeader(title: 'Defaults', icon: Icons.tune), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Default Tool', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), DropdownButtonFormField( initialValue: settings.defaultTool, decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, ), items: PenTool.values.map((tool) { return DropdownMenuItem( value: tool, child: Text(tool.name), ); }).toList(), onChanged: (tool) { if (tool != null) settings.setDefaultTool(tool); }, ), const SizedBox(height: 16), const Text( 'Default Color', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Row( children: [ MouseRegion( cursor: SystemMouseCursors.click, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () => _showColorPicker( context, settings.defaultColor, settings.setDefaultColor, ), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: settings.defaultColor, borderRadius: BorderRadius.circular(8), border: Border.all(color: colorScheme.outline), ), ), ), ), const SizedBox(width: 12), Text( '#${settings.defaultColor.toARGB32().toRadixString(16).padLeft(8, '0').toUpperCase()}', style: const TextStyle(fontFamily: 'monospace'), ), ], ), const SizedBox(height: 16), const Text( 'Default Stroke Width', style: TextStyle(fontWeight: FontWeight.w500), ), Slider( value: settings.defaultStrokeWidth, min: 1.0, max: 20.0, divisions: 19, label: settings.defaultStrokeWidth.toStringAsFixed(1), onChanged: settings.setDefaultStrokeWidth, ), const SizedBox(height: 16), const Text( 'Pressure Curve', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), DropdownButtonFormField( initialValue: settings.defaultPressureCurve, decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, ), items: PressureCurveType.values.map((curve) { return DropdownMenuItem( value: curve, child: Text(curve.name), ); }).toList(), onChanged: (curve) { if (curve != null) { settings.setDefaultPressureCurve(curve); } }, ), const SizedBox(height: 16), const Text( 'Stabilization', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), DropdownButtonFormField( initialValue: settings.defaultStabilization, decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, ), items: StabilizationLevel.values.map((level) { return DropdownMenuItem( value: level, child: Text(level.name), ); }).toList(), onChanged: (level) { if (level != null) settings.setDefaultStabilization(level); }, ), ], ), ), const Divider(), _SectionHeader(title: 'Appearance', icon: Icons.palette), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Theme Mode', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), SegmentedButton( segments: [ ButtonSegment( value: ThemeMode.system, label: Text(AppLocalizations.of(context).themeSystem), icon: const Icon(Icons.brightness_auto), ), ButtonSegment( value: ThemeMode.light, label: Text(AppLocalizations.of(context).themeLight), icon: const Icon(Icons.light_mode), ), ButtonSegment( value: ThemeMode.dark, label: Text(AppLocalizations.of(context).themeDark), icon: const Icon(Icons.dark_mode), ), ], selected: {settings.themeMode}, onSelectionChanged: (modes) { settings.setThemeMode(modes.first); }, ), const SizedBox(height: 16), const Text( 'Color Scheme Seed', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Row( children: [ MouseRegion( cursor: SystemMouseCursors.click, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () => _showColorPicker( context, settings.colorSchemeSeed, settings.setColorSchemeSeed, ), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: settings.colorSchemeSeed, borderRadius: BorderRadius.circular(8), border: Border.all(color: colorScheme.outline), ), ), ), ), const SizedBox(width: 12), Text(AppLocalizations.of(context).seedColorDesc), ], ), ], ), ), const Divider(), _SectionHeader(title: 'Vault', icon: Icons.folder_special), const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: _VaultSettings(), ), const Divider(), _SectionHeader(title: 'About', icon: Icons.info), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'BadNote v0.1.0', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Text( 'Local-first Surface Pen note-taking with PDF/PPT annotation. ' 'OCR and search run entirely on your device.', style: TextStyle( fontSize: 13, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), OutlinedButton.icon( onPressed: () => _confirmClearData(context, ref), icon: const Icon(Icons.delete_forever, color: Colors.red), label: const Text( 'Clear All Local Settings', style: TextStyle(color: Colors.red), ), ), ], ), ), const SizedBox(height: 32), ], ), ); } } /// Shows the current vault folder path and lets the user re-pick it. Re-uses /// the same `getDirectoryPath` flow as the first-run [VaultSetupScreen], /// persisting the choice through [VaultService.setVaultRoot]. class _VaultSettings extends StatefulWidget { const _VaultSettings(); @override State<_VaultSettings> createState() => _VaultSettingsState(); } class _VaultSettingsState extends State<_VaultSettings> { VaultService? _vault; String? _path; bool _busy = false; @override void initState() { super.initState(); _load(); } Future _load() async { final vault = await VaultService.getInstance(); if (!mounted) return; setState(() { _vault = vault; _path = vault.vaultRoot; }); } Future _changeFolder() async { final vault = _vault; if (vault == null) return; final l = AppLocalizations.of(context); final messenger = ScaffoldMessenger.of(context); setState(() => _busy = true); try { final path = await FilePicker.platform.getDirectoryPath( dialogTitle: l.vaultSetupTitle, lockParentWindow: true, ); if (path != null) { await vault.setVaultRoot(path); if (!mounted) return; setState(() => _path = path); messenger.showSnackBar(SnackBar(content: Text(l.vaultUpdated))); } } catch (e) { if (!mounted) return; messenger.showSnackBar( SnackBar(content: Text(l.vaultPickFailed(e.toString()))), ); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final l = AppLocalizations.of(context); final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l.vaultFolderLabel, style: const TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Text( (_path == null || _path!.isEmpty) ? l.vaultNoneSelected : _path!, style: TextStyle( fontSize: 13, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: _busy ? null : _changeFolder, icon: const Icon(Icons.drive_folder_upload), label: Text(l.vaultChangeFolder), ), ], ); } } class _SectionHeader extends StatelessWidget { final String title; final IconData icon; const _SectionHeader({required this.title, required this.icon}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Row( children: [ Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 8), Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ], ), ); } }