import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../cache/audio_cache_manager.dart'; import '../cache/cache_settings_provider.dart'; import '../cache/sync_controller.dart'; import '../theme/theme_extension.dart'; /// Settings card: usage display, cap selector, prefetch window selector, /// cache-liked toggle, Clear cache + Sync now buttons. class StorageSection extends ConsumerStatefulWidget { const StorageSection({super.key}); @override ConsumerState createState() => _StorageSectionState(); } class _StorageSectionState extends ConsumerState { int? _usageBytes; bool _syncing = false; @override void initState() { super.initState(); _refreshUsage(); } Future _refreshUsage() async { final mgr = ref.read(audioCacheManagerProvider); final used = await mgr.usageBytes(); if (mounted) setState(() => _usageBytes = used); } String _fmtBytes(int? n) { if (n == null) return '—'; if (n == 0) return '0 B'; if (n < 1024) return '$n B'; if (n < 1024 * 1024) return '${(n / 1024).toStringAsFixed(1)} KB'; if (n < 1024 * 1024 * 1024) return '${(n / 1024 / 1024).toStringAsFixed(1)} MB'; return '${(n / 1024 / 1024 / 1024).toStringAsFixed(2)} GB'; } @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; final settings = ref.watch(cacheSettingsProvider); return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Card( color: fs.iron, child: Padding( padding: const EdgeInsets.all(16), child: settings.when( loading: () => const Padding( padding: EdgeInsets.all(8), child: Center(child: CircularProgressIndicator()), ), error: (e, _) => Text('$e', style: TextStyle(color: fs.error)), data: (s) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Storage', style: TextStyle( color: fs.parchment, fontSize: 18, fontWeight: FontWeight.w500)), const SizedBox(height: 12), Row(children: [ Text('Cache usage', style: TextStyle(color: fs.ash)), const Spacer(), Text( '${_fmtBytes(_usageBytes)} / ' '${s.capBytes == 0 ? "unlimited" : _fmtBytes(s.capBytes)}', style: TextStyle(color: fs.parchment), ), ]), const SizedBox(height: 16), _capSelector(s, fs), const SizedBox(height: 8), _prefetchSelector(s, fs), const SizedBox(height: 8), SwitchListTile( key: const Key('cache_liked_toggle'), contentPadding: EdgeInsets.zero, title: Text('Cache liked tracks', style: TextStyle(color: fs.parchment)), value: s.cacheLikedTracks, onChanged: (v) => ref .read(cacheSettingsProvider.notifier) .setCacheLikedTracks(v), ), const SizedBox(height: 12), Wrap(spacing: 8, runSpacing: 8, children: [ OutlinedButton( key: const Key('clear_cache_button'), onPressed: _confirmClear, child: const Text('Clear cache'), ), OutlinedButton.icon( key: const Key('sync_now_button'), onPressed: _syncing ? null : () async { setState(() => _syncing = true); try { await ref .read(syncControllerProvider.notifier) .sync(); } finally { if (mounted) setState(() => _syncing = false); } }, icon: _syncing ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.sync, size: 16), label: const Text('Sync now'), ), ]), ], ), ), ), ), ); } Widget _capSelector(CacheSettings s, FabledSwordTheme fs) { const options = [ (1024 * 1024 * 1024, '1 GB'), (5 * 1024 * 1024 * 1024, '5 GB'), (10 * 1024 * 1024 * 1024, '10 GB'), (25 * 1024 * 1024 * 1024, '25 GB'), (0, 'Unlimited'), ]; return Row(children: [ Expanded(child: Text('Cache size limit', style: TextStyle(color: fs.ash))), DropdownButton( key: const Key('cap_selector'), value: options.any((o) => o.$1 == s.capBytes) ? s.capBytes : 5 * 1024 * 1024 * 1024, items: options .map((o) => DropdownMenuItem(value: o.$1, child: Text(o.$2))) .toList(), onChanged: (v) async { if (v == null) return; await ref.read(cacheSettingsProvider.notifier).setCapBytes(v); if (v > 0) { await ref.read(audioCacheManagerProvider).evict(targetBytes: v); } await _refreshUsage(); }, ), ]); } Widget _prefetchSelector(CacheSettings s, FabledSwordTheme fs) { const options = [1, 3, 5, 7, 10]; return Row(children: [ Expanded(child: Text('Pre-fetch ahead', style: TextStyle(color: fs.ash))), DropdownButton( key: const Key('prefetch_selector'), value: options.contains(s.prefetchWindow) ? s.prefetchWindow : 5, items: options .map((n) => DropdownMenuItem(value: n, child: Text('$n tracks'))) .toList(), onChanged: (v) async { if (v == null) return; await ref.read(cacheSettingsProvider.notifier).setPrefetchWindow(v); }, ), ]); } Future _confirmClear() async { final fs = Theme.of(context).extension()!; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Clear cache?'), content: const Text( 'This deletes all cached audio files (including manually downloaded ones). ' 'The next play of any track will re-download from the server.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel'), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: fs.error), onPressed: () => Navigator.pop(ctx, true), child: const Text('Clear'), ), ], ), ); if (confirmed != true) return; await ref.read(audioCacheManagerProvider).clearAll(); await _refreshUsage(); } }