import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../providers/theme_provider.dart'; import '../providers/media_library_provider.dart'; import '../widgets/app_log_dialog.dart'; import '../core/http/dio_client.dart'; import '../core/storage/storage_manager.dart'; enum _SettingsTab { general, network, media, diagnostics } class SettingsDialog extends ConsumerStatefulWidget { const SettingsDialog({super.key}); @override ConsumerState createState() => _SettingsDialogState(); } class _SettingsDialogState extends ConsumerState { _SettingsTab _activeTab = _SettingsTab.general; var _saving = false; final _tmdbApiKeyController = TextEditingController(); final _tmdbImageProxyController = TextEditingController(); final _httpProxyHostController = TextEditingController(); final _httpProxyPortController = TextEditingController(); final _scanConcurrencyController = TextEditingController(); final _transferConcurrencyController = TextEditingController(); final _cacheTTLController = TextEditingController(); final _cloudIndexRefreshController = TextEditingController(); final _pageSizeController = TextEditingController(); @override void initState() { super.initState(); _tmdbApiKeyController.text = StorageManager.get(StorageKeys.tmdbApiKey) ?? ''; _tmdbImageProxyController.text = StorageManager.get(StorageKeys.tmdbImageProxy) ?? 'https://wsrv.nl'; _httpProxyHostController.text = StorageManager.networkProxyHost; _httpProxyPortController.text = StorageManager.networkProxyPort; _scanConcurrencyController.text = StorageManager.get(StorageKeys.mediaScanConcurrency) ?? '3'; _transferConcurrencyController.text = StorageManager.get(StorageKeys.fastTransferConcurrency) ?? '3'; _cacheTTLController.text = StorageManager.get(StorageKeys.fileCacheTTLMinutes) ?? '3'; _cloudIndexRefreshController.text = StorageManager.get(StorageKeys.cloudIndexRefreshMinutes) ?? '30'; _pageSizeController.text = StorageManager.get(StorageKeys.defaultFilePageSize) ?? '50'; } @override void dispose() { _tmdbApiKeyController.dispose(); _tmdbImageProxyController.dispose(); _httpProxyHostController.dispose(); _httpProxyPortController.dispose(); _scanConcurrencyController.dispose(); _transferConcurrencyController.dispose(); _cacheTTLController.dispose(); _cloudIndexRefreshController.dispose(); _pageSizeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final themeState = ref.watch(themeProvider); final mediaState = ref.watch(mediaLibraryProvider); final compact = MediaQuery.sizeOf(context).width < 760; return ShadDialog( title: Row( children: [ Icon(Icons.settings_outlined, size: 19, color: cs.primary), const SizedBox(width: 10), const Text('设置'), ], ), description: const Text('应用外观、网络任务与影视资料'), actions: [ ShadButton.outline( onPressed: _saving ? null : () => Navigator.of(context).pop(), child: const Text('取消'), ), ShadButton( onPressed: _saving ? null : _saveAndClose, leading: _saving ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: cs.primaryForeground, ), ) : const Icon(Icons.check_rounded, size: 16), child: Text(_saving ? '正在保存' : '保存设置'), ), ], child: SizedBox( width: compact ? MediaQuery.sizeOf(context).width - 32 : 720, child: ShadTabs<_SettingsTab>( value: _activeTab, onChanged: (value) => setState(() => _activeTab = value), scrollable: compact, tabBarConstraints: const BoxConstraints(maxWidth: 720), contentConstraints: const BoxConstraints(maxWidth: 720), tabs: [ ShadTab( value: _SettingsTab.general, leading: const Icon(Icons.tune_rounded, size: 15), content: _tabScroll( _SettingsSection( icon: Icons.palette_outlined, title: '外观与浏览', child: Column( children: [ _SettingsRow( icon: Icons.light_mode_rounded, label: '主题模式', child: ShadSelect( initialValue: _themeModeToString( themeState.themeMode, ), minWidth: 180, placeholder: const Text('选择主题'), selectedOptionBuilder: (context, value) => Text( _themeModeToString(_stringToThemeMode(value)), ), options: const [ ShadOption(value: 'light', child: Text('浅色')), ShadOption(value: 'dark', child: Text('深色')), ShadOption(value: 'system', child: Text('跟随系统')), ], onChanged: (value) { if (value != null) { ref .read(themeProvider.notifier) .setThemeMode(_stringToThemeMode(value)); } }, ), ), _SettingsRow( icon: Icons.format_list_numbered_rounded, label: '默认分页大小', child: _numberInput(_pageSizeController), ), ], ), ), ), child: const Text('通用'), ), ShadTab( value: _SettingsTab.network, leading: const Icon(Icons.network_check_rounded, size: 15), content: _tabScroll( Column( children: [ _SettingsSection( icon: Icons.http_rounded, title: '网络代理', child: Column( children: [ _SettingsRow( icon: Icons.lan_outlined, label: '代理地址', child: _textInput( _httpProxyHostController, placeholder: '127.0.0.1', ), ), _SettingsRow( icon: Icons.tag_rounded, label: '代理端口', child: _numberInput( _httpProxyPortController, placeholder: '7890', ), ), ], ), ), const SizedBox(height: 22), _SettingsSection( icon: Icons.bolt_outlined, title: '任务与缓存', child: Column( children: [ _SettingsRow( icon: Icons.memory_rounded, label: '媒体扫描并发', child: _numberInput(_scanConcurrencyController), ), _SettingsRow( icon: Icons.bolt_rounded, label: '秒传并发', child: _numberInput(_transferConcurrencyController), ), _SettingsRow( icon: Icons.cached_rounded, label: '文件缓存分钟', child: _numberInput(_cacheTTLController), ), _SettingsRow( icon: Icons.cloud_sync_rounded, label: '全盘索引间隔', child: _numberInput(_cloudIndexRefreshController), ), _SettingsRow( icon: Icons.refresh_rounded, label: '全盘文件索引', child: ShadButton.outline( size: ShadButtonSize.sm, onPressed: mediaState.isRefreshingCloudIndex ? null : () => unawaited( ref .read(mediaLibraryProvider.notifier) .refreshGlobalCloudIndex(force: true), ), leading: mediaState.isRefreshingCloudIndex ? SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: cs.primary, ), ) : const Icon(Icons.refresh_rounded, size: 15), child: Text( mediaState.isRefreshingCloudIndex ? '刷新中' : '立即刷新', ), ), ), ], ), ), ], ), ), child: const Text('网络与任务'), ), ShadTab( value: _SettingsTab.media, leading: const Icon(Icons.movie_filter_outlined, size: 15), content: _tabScroll( _SettingsSection( icon: Icons.movie_filter_outlined, title: 'TMDB', child: Column( children: [ _SettingsRow( icon: Icons.key_rounded, label: 'API Key', child: _textInput( _tmdbApiKeyController, placeholder: '输入 TMDB API Key', ), ), _SettingsRow( icon: Icons.image_outlined, label: '图片加速地址', child: _textInput( _tmdbImageProxyController, placeholder: 'https://wsrv.nl', ), ), ], ), ), ), child: const Text('影视资料'), ), ShadTab( value: _SettingsTab.diagnostics, leading: const Icon(Icons.monitor_heart_outlined, size: 15), content: _tabScroll( Column( children: [ _SettingsSection( icon: Icons.sticky_note_2_outlined, title: '运行诊断', child: _SettingsRow( icon: Icons.subject_rounded, label: '运行日志', child: ShadButton.outline( size: ShadButtonSize.sm, onPressed: () => showShadDialog( context: context, builder: (_) => const AppLogDialog(), ), leading: const Icon( Icons.open_in_new_rounded, size: 15, ), child: const Text('查看日志'), ), ), ), const SizedBox(height: 22), _SettingsSection( icon: Icons.info_outline_rounded, title: '应用信息', child: _SettingsRow( icon: Icons.cloud_outlined, label: '版本', child: Text( 'v1.0.0', style: TextStyle( fontSize: 13, color: cs.mutedForeground, ), ), ), ), ], ), ), child: const Text('诊断'), ), ], ), ), ); } Widget _tabScroll(Widget child) { final availableHeight = MediaQuery.sizeOf(context).height - 270; return SizedBox( height: availableHeight.clamp(260.0, 420.0), child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 16), child: child, ), ); } Widget _textInput( TextEditingController controller, { required String placeholder, }) => SizedBox( width: double.infinity, child: ShadInput(controller: controller, placeholder: Text(placeholder)), ); Widget _numberInput( TextEditingController controller, { String? placeholder, }) => SizedBox( width: double.infinity, child: ShadInput( controller: controller, placeholder: placeholder == null ? null : Text(placeholder), keyboardType: TextInputType.number, ), ); Future _saveSettings() async { await Future.wait([ StorageManager.set( StorageKeys.tmdbApiKey, _tmdbApiKeyController.text.trim(), ), StorageManager.set( StorageKeys.tmdbImageProxy, _tmdbImageProxyController.text.trim(), ), StorageManager.set( StorageKeys.httpProxyHost, _httpProxyHostController.text.trim(), ), StorageManager.set( StorageKeys.httpProxyPort, _httpProxyPortController.text.trim(), ), StorageManager.delete(StorageKeys.tmdbProxyHost), StorageManager.delete(StorageKeys.tmdbProxyPort), StorageManager.set( StorageKeys.mediaScanConcurrency, _scanConcurrencyController.text.trim(), ), StorageManager.set( StorageKeys.fastTransferConcurrency, _transferConcurrencyController.text.trim(), ), StorageManager.set( StorageKeys.fileCacheTTLMinutes, _cacheTTLController.text.trim(), ), StorageManager.set( StorageKeys.cloudIndexRefreshMinutes, _cloudIndexRefreshController.text.trim(), ), StorageManager.set( StorageKeys.defaultFilePageSize, _pageSizeController.text.trim(), ), ]); DioClient.updateNetworkProxy(); ref.read(mediaLibraryProvider.notifier).updateCloudIndexRefreshSchedule(); } Future _saveAndClose() async { setState(() => _saving = true); try { await _saveSettings(); if (!mounted) return; ShadSonner.maybeOf(context)?.show( const ShadToast( title: Text('设置已保存'), description: Text('新的配置已应用到后续任务。'), showCloseIconOnlyWhenHovered: false, ), ); Navigator.of(context).pop(); } catch (error) { if (!mounted) return; ShadSonner.maybeOf(context)?.show( ShadToast.destructive( title: const Text('保存设置失败'), description: Text(error.toString()), showCloseIconOnlyWhenHovered: false, ), ); setState(() => _saving = false); } } String _themeModeToString(ThemeMode mode) { switch (mode) { case ThemeMode.light: return 'light'; case ThemeMode.dark: return 'dark'; case ThemeMode.system: return 'system'; } } ThemeMode _stringToThemeMode(String value) { switch (value) { case 'light': return ThemeMode.light; case 'dark': return ThemeMode.dark; default: return ThemeMode.system; } } } class _SettingsSection extends StatelessWidget { final IconData icon; final String title; final Widget child; const _SettingsSection({ required this.icon, required this.title, required this.child, }); @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 14), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: cs.muted.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(8), border: Border.all(color: cs.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 30, height: 30, alignment: Alignment.center, decoration: BoxDecoration( color: cs.primary.withValues(alpha: 0.11), borderRadius: BorderRadius.circular(7), ), child: Icon(icon, size: 16, color: cs.primary), ), const SizedBox(width: 10), Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: cs.foreground, ), ), ], ), const SizedBox(height: 12), const ShadSeparator.horizontal(), const SizedBox(height: 4), child, ], ), ); } } class _SettingsRow extends StatelessWidget { final IconData icon; final String label; final Widget child; const _SettingsRow({ required this.icon, required this.label, required this.child, }); @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final labelWidget = Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: cs.mutedForeground), const SizedBox(width: 10), Text(label, style: TextStyle(fontSize: 13, color: cs.foreground)), ], ); return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ labelWidget, const SizedBox(height: 8), SizedBox(width: double.infinity, child: child), ], ), ); } }