import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; import '../../main.dart' show routeObserver; import '../../models/data_models.dart'; import '../../providers/app_provider.dart'; import '../../utils/user_prefs.dart'; import '../../utils/toast_util.dart'; import '../settings/recycle_bin_page.dart'; import '../sync/backup_page.dart'; import '../../widgets/fade_in_local_image.dart'; import '../explore/statistics_page.dart'; import '../settings/tag_management_page.dart'; import '../explore/stroll_page.dart'; import '../sync/cloud_sync_page.dart'; import 'settings_page.dart'; /// 个人中心页面 class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @override State createState() => _ProfilePageState(); } class _ProfilePageState extends State with RouteAware { final ImagePicker _picker = ImagePicker(); final UserPrefs _userPrefs = UserPrefs(); String _nickname = 'Mook'; String _motto = '好运不会眷顾一无所有之人。'; String? _avatarPath; @override void initState() { super.initState(); _loadUserData(); } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context)!); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } @override void didPopNext() { // 从其他页面返回时刷新用户数据(头像、昵称等) _loadUserData(); } Future _loadUserData() async { try { setState(() { _nickname = _userPrefs.nickname; _motto = _userPrefs.motto; _avatarPath = _userPrefs.avatarPath; }); } catch (_) {} } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Column( children: [ AppBar( titleSpacing: 8, leadingWidth: 44, leading: Builder( builder: (context) => IconButton( icon: Icon(Icons.menu, color: colors.onSurface), onPressed: () => Scaffold.of(context).openDrawer(), ), ), title: const Text('我的'), ), Expanded( child: Consumer( builder: (context, provider, child) { final movies = provider.movies.where((m) => !m.isDeleted).toList(); final books = provider.books.where((b) => !b.isDeleted).toList(); final notes = provider.notes.where((n) => !n.isDeleted).toList(); final games = provider.games.where((g) => !g.isDeleted).toList(); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHero(movies, books, notes, games), const SizedBox(height: 20), if (_userPrefs.showMovieTab) ...[ _buildModuleHeader('影视'), _buildMovieModule(movies), const SizedBox(height: 20), ], if (_userPrefs.showBookTab) ...[ _buildModuleHeader('阅读'), _buildBookModule(books), const SizedBox(height: 20), ], if (_userPrefs.showNoteTab) ...[ _buildModuleHeader('笔记'), _buildNoteModule(notes), const SizedBox(height: 20), ], _buildTagsSection(movies, books, notes), const SizedBox(height: 20), _buildToolsGrid(context), const SizedBox(height: 120), ], ), ); }, ), ), ], ); } // ─── Hero 区域 ────────────────────────────────────────────────────── Widget _buildHero(List movies, List books, List notes, List games) { final colors = Theme.of(context).colorScheme; final coverPaths = [ ...movies .where((m) => m.posterPath != null && m.posterPath!.isNotEmpty) .map((m) => m.posterPath!), ...books .where((b) => b.coverPath != null && b.coverPath!.isNotEmpty) .map((b) => b.coverPath!), ]..shuffle(); final hasData = coverPaths.length >= 4; return Container( margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), clipBehavior: Clip.antiAlias, decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), child: Stack( children: [ if (hasData) Positioned.fill(child: _buildPosterMosaic(coverPaths)) else Positioned.fill( child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [colors.primary, colors.primary.withValues(alpha: 0.6)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ))), Positioned.fill( child: Container( color: hasData ? Colors.black.withValues(alpha: 0.35) : colors.surface.withValues(alpha: 0.82))), Padding( padding: EdgeInsets.fromLTRB( 20, MediaQuery.of(context).padding.top + 20, 20, 20), child: Column( children: [ Row( children: [ GestureDetector( onTap: _pickAvatar, child: Container( width: 56, height: 56, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: hasData ? Colors.white.withValues(alpha: 0.6) : colors.outlineVariant, width: 1.5), ), clipBehavior: Clip.antiAlias, child: _avatarPath != null && _avatarPath!.isNotEmpty ? FadeInLocalImage( path: _avatarPath, fit: BoxFit.cover, errorWidget: Icon(Icons.person_outline, size: 28, color: hasData ? Colors.white.withValues(alpha: 0.5) : colors.onSurface .withValues(alpha: 0.3))) : Container( color: hasData ? Colors.white.withValues(alpha: 0.1) : colors.surfaceContainerHighest, child: Icon(Icons.person_outline, size: 28, color: hasData ? Colors.white.withValues(alpha: 0.5) : colors.onSurface .withValues(alpha: 0.3))), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_nickname, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: hasData ? Colors.white : colors.onSurface)), const SizedBox(height: 4), Text(_motto, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: hasData ? Colors.white.withValues(alpha: 0.85) : colors.onSurface .withValues(alpha: 0.6))), ], ), ), ], ), const SizedBox(height: 20), Row( children: [ if (_userPrefs.showMovieTab) _buildHeroStat(_formatCount(movies.length), '观影', hasData), if (_userPrefs.showBookTab) _buildHeroStat(_formatCount(books.length), '阅读', hasData), if (_userPrefs.showNoteTab) _buildHeroStat(_formatCount(notes.length), '笔记', hasData), if (_userPrefs.showGameTab) _buildHeroStat(_formatCount(games.length), '游戏', hasData), ], ), ], ), ), ], ), ); } Widget _buildPosterMosaic(List paths) { final posters = paths.take(12).toList(); while (posters.length < 12) posters.add(posters[posters.length % paths.length]); return GridView.builder( physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 2, crossAxisSpacing: 2, ), itemCount: 12, itemBuilder: (_, i) => FadeInLocalImage(path: posters[i], fit: BoxFit.cover), ); } Widget _buildHeroStat(String value, String label, bool hasData) { return Expanded( child: Column( children: [ Text(value, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: hasData ? Colors.white : Theme.of(context).colorScheme.onSurface)), const SizedBox(height: 2), Text(label, style: TextStyle( fontSize: 11, color: hasData ? Colors.white.withValues(alpha: 0.85) : Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.6))), ], ), ); } // ─── 模块标题 ────────────────────────────────────────────────────── Widget _buildModuleHeader(String title) { final colors = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text(title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)), ); } // ─── 影视模块 ────────────────────────────────────────────────────── Widget _buildMovieModule(List movies) { final colors = Theme.of(context).colorScheme; final watched = movies.where((m) => m.status == 'watched').length; final watching = movies.where((m) => m.status == 'watching').length; final wantTo = movies.where((m) => m.status == 'want_to_watch').length; final recent = movies.toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row(children: [ _buildStatusTag('已看', watched, true, colors), const SizedBox(width: 10), _buildStatusTag('在看', watching, false, colors), const SizedBox(width: 10), _buildStatusTag('想看', wantTo, false, colors), ]), ), const SizedBox(height: 10), if (recent.isNotEmpty) SizedBox( height: 120, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 20), itemCount: recent.take(15).length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) => _buildCoverCard( title: recent[i].title, imagePath: recent[i].posterPath, onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: recent[i]), ), ), ) else _buildEmptyHint('暂无影视记录'), ], ); } // ─── 阅读模块 ────────────────────────────────────────────────────── Widget _buildBookModule(List books) { final colors = Theme.of(context).colorScheme; final read = books.where((b) => b.status == 'read').length; final reading = books.where((b) => b.status == 'reading').length; final wantTo = books.where((b) => b.status == 'want_to_read').length; final recent = books.toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row(children: [ _buildStatusTag('已读', read, true, colors), const SizedBox(width: 10), _buildStatusTag('在读', reading, false, colors), const SizedBox(width: 10), _buildStatusTag('想读', wantTo, false, colors), ]), ), const SizedBox(height: 10), if (recent.isNotEmpty) SizedBox( height: 120, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 20), itemCount: recent.take(15).length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) => _buildCoverCard( title: recent[i].title, imagePath: recent[i].coverPath, onTap: () => Navigator.pushNamed(context, '/book-detail', arguments: recent[i]), ), ), ) else _buildEmptyHint('暂无阅读记录'), ], ); } // ─── 笔记模块 ────────────────────────────────────────────────────── Widget _buildNoteModule(List notes) { final recent = notes.toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); if (recent.isEmpty) return _buildEmptyHint('暂无笔记记录'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), SizedBox( height: 130, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 20), itemCount: recent.take(10).length, separatorBuilder: (_, __) => const SizedBox(width: 10), itemBuilder: (_, i) => _buildNoteCard( title: recent[i].title, content: recent[i].content, onTap: () => Navigator.pushNamed(context, '/note-detail', arguments: recent[i]), ), ), ), ], ); } Widget _buildNoteCard({ required String title, required String content, VoidCallback? onTap, }) { final colors = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Container( width: 115, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title.isNotEmpty) ...[ Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), const SizedBox(height: 6), ], Expanded( child: Text(content, maxLines: title.isNotEmpty ? 5 : 6, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, height: 1.5, color: colors.onSurface.withValues(alpha: 0.55))), ), ], ), ), ); } Widget _buildStatusTag( String label, int count, bool active, ColorScheme colors) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration( shape: BoxShape.circle, color: active ? colors.primary : colors.onSurface.withValues(alpha: 0.25), ), ), const SizedBox(width: 4), Text('$label $count', style: TextStyle( fontSize: 12, fontWeight: active ? FontWeight.w600 : FontWeight.w400, color: active ? colors.onSurface : colors.onSurface.withValues(alpha: 0.5), )), ], ); } Widget _buildCoverCard( {required String title, String? imagePath, VoidCallback? onTap}) { final colors = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: SizedBox( width: 78, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Container( width: 78, decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), clipBehavior: Clip.antiAlias, child: imagePath != null && imagePath.isNotEmpty ? FadeInLocalImage( path: imagePath, fit: BoxFit.cover, errorWidget: Icon(Icons.image_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.2))) : Icon(Icons.image_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.2)), ), ), const SizedBox(height: 4), Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 10, color: colors.onSurface.withValues(alpha: 0.6))), ], ), ), ); } Widget _buildEmptyHint(String text) { final colors = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: Center( child: Text(text, style: TextStyle( fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3)))), ); } // ─── 标签模块 ────────────────────────────────────────────────────── Widget _buildTagsSection( List movies, List books, List notes) { final colors = Theme.of(context).colorScheme; final freq = {}; for (final m in movies) { for (final g in m.genres) { freq[g] = (freq[g] ?? 0) + 1; } } for (final b in books) { for (final g in b.genres) { freq[g] = (freq[g] ?? 0) + 1; } } for (final n in notes) { for (final t in n.tags) { freq[t] = (freq[t] ?? 0) + 1; } } final sorted = freq.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); final topTags = sorted.take(10).map((e) => e.key).toList(); if (topTags.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Text('常用标签', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)), const Spacer(), GestureDetector( onTap: () => Navigator.push( context, MaterialPageRoute( builder: (_) => const TagManagementPage())), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text('管理', style: TextStyle( fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.3)), ], ), ), ], ), ), const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Wrap( spacing: 8, runSpacing: 8, children: topTags .map((tag) => Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5), decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Text(tag, style: TextStyle( fontSize: 12, color: colors.onSurface.withValues(alpha: 0.7))), )) .toList(), ), ), ], ); } // ─── 工具栏 ──────────────────────────────────────────────────────── Widget _buildToolsGrid(BuildContext context) { final colors = Theme.of(context).colorScheme; final tools = [ ( Icons.explore_outlined, '漫步', () => Navigator.push( context, MaterialPageRoute(builder: (_) => const StrollPage())) ), ( Icons.analytics_outlined, '统计', () => Navigator.push( context, MaterialPageRoute(builder: (_) => const StatisticsPage())) ), (Icons.backup_outlined, '备份', () => _showBackupOptions(context)), ( Icons.delete_outline, '回收', () => Navigator.push( context, MaterialPageRoute(builder: (_) => const RecycleBinPage())) ), ( Icons.settings_outlined, '设置', () => Navigator.push( context, MaterialPageRoute(builder: (_) => const SettingsPage())) ), (Icons.feedback_outlined, '反馈', () => _showFeedbackDialog(context)), ]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( decoration: BoxDecoration( color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), ), child: Column( children: tools.asMap().entries.map((e) { final isLast = e.key == tools.length - 1; return Column( children: [ InkWell( onTap: e.value.$3, borderRadius: BorderRadius.only( topLeft: e.key == 0 ? const Radius.circular(12) : Radius.zero, topRight: e.key == 0 ? const Radius.circular(12) : Radius.zero, bottomLeft: isLast ? const Radius.circular(12) : Radius.zero, bottomRight: isLast ? const Radius.circular(12) : Radius.zero, ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12), child: Row( children: [ Icon(e.value.$1, size: 18, color: colors.onSurface.withValues(alpha: 0.6)), const SizedBox(width: 12), Expanded( child: Text(e.value.$2, style: TextStyle( fontSize: 13, color: colors.onSurface .withValues(alpha: 0.7)))), Icon(Icons.chevron_right, size: 16, color: colors.onSurface.withValues(alpha: 0.2)), ], ), ), ), if (!isLast) Divider( height: 1, indent: 46, endIndent: 16, color: colors.outlineVariant), ], ); }).toList(), ), ), ); } // ─── 辅助方法 ────────────────────────────────────────────────────── void _push(BuildContext context, Widget page) { Navigator.push(context, MaterialPageRoute(builder: (_) => page)); } String _formatCount(int count) { if (count >= 10000) return '${(count / 10000).toStringAsFixed(1)}万'; if (count >= 1000) return '${(count / 1000).toStringAsFixed(1)}k'; return count.toString(); } // ─── 反馈弹窗 ──────────────────────────────────────────────────────── void _showFeedbackDialog(BuildContext context) { final colors = Theme.of(context).colorScheme; final email = 'dellevin99@gmail.com'; showModalBottomSheet( context: context, backgroundColor: colors.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (ctx) => SafeArea( child: Column(mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 4, margin: const EdgeInsets.only(top: 12, bottom: 16), decoration: BoxDecoration( color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Align( alignment: Alignment.centerLeft, child: Text('反馈', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colors.surfaceContainerHighest.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(Icons.email_outlined, size: 20, color: colors.primary.withValues(alpha: 0.8)), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('作者邮箱', style: TextStyle( fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))), const SizedBox(height: 2), Text(email, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)), ], ), ), GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: email)); ToastUtil.show(context, '已复制到剪贴板'); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: colors.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.copy, size: 14, color: colors.primary), const SizedBox(width: 4), Text('复制', style: TextStyle( fontSize: 12, color: colors.primary, fontWeight: FontWeight.w600)), ], ), ), ), ], ), ), ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colors.surfaceContainerHighest.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(Icons.group_outlined, size: 20, color: colors.primary.withValues(alpha: 0.8)), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('QQ 群', style: TextStyle( fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))), const SizedBox(height: 2), Text('1087203310', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)), ], ), ), GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: '1087203310')); ToastUtil.show(context, '已复制到剪贴板'); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: colors.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.copy, size: 14, color: colors.primary), const SizedBox(width: 4), Text('复制', style: TextStyle( fontSize: 12, color: colors.primary, fontWeight: FontWeight.w600)), ], ), ), ), ], ), ), ), const SizedBox(height: 16), ]), ), ); } // ─── 头像 ──────────────────────────────────────────────────────────── Future _pickAvatar() async { final colors = Theme.of(context).colorScheme; await showModalBottomSheet( context: context, backgroundColor: colors.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (ctx) => SafeArea( child: Column(mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 4, margin: const EdgeInsets.only(top: 12, bottom: 16), decoration: BoxDecoration( color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Align( alignment: Alignment.centerLeft, child: Text('更换头像', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))), const SizedBox(height: 8), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.folder_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.6))), title: Text('从文件管理器选择', style: TextStyle(fontSize: 14, color: colors.onSurface)), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)), onTap: () async { Navigator.pop(ctx); await _pickAvatarFromFile(); }, ), if (_avatarPath != null && _avatarPath!.isNotEmpty) ...[ Divider( height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.delete_outline, size: 20, color: colors.error)), title: Text('移除头像', style: TextStyle(fontSize: 14, color: colors.error)), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)), onTap: () async { Navigator.pop(ctx); await _userPrefs.clearAvatarPath(); setState(() => _avatarPath = null); }, ), ], const SizedBox(height: 8), ]), ), ); } Future _pickAvatarFromFile() async { try { final pickedFile = await _picker.pickImage( source: ImageSource.gallery, maxWidth: 400, maxHeight: 400, imageQuality: 85); if (pickedFile != null) { final appDir = await getApplicationDocumentsDirectory(); final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; final savedPath = path.join(appDir.path, 'avatars', fileName); final avatarDir = Directory(path.join(appDir.path, 'avatars')); if (!await avatarDir.exists()) await avatarDir.create(recursive: true); await File(pickedFile.path).copy(savedPath); await _userPrefs.setAvatarPath(savedPath); setState(() => _avatarPath = savedPath); } } catch (e) { if (mounted) ToastUtil.show(context, '选择头像失败'); } } // ─── 备份弹窗 ──────────────────────────────────────────────────────── void _showBackupOptions(BuildContext context) { final colors = Theme.of(context).colorScheme; showModalBottomSheet( context: context, backgroundColor: colors.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (ctx) => Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 4, decoration: BoxDecoration( color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))), const SizedBox(height: 20), Align( alignment: Alignment.centerLeft, child: Text('选择备份方式', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface))), const SizedBox(height: 16), ListTile( contentPadding: EdgeInsets.zero, leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.folder_outlined, color: colors.onSurface.withValues(alpha: 0.6))), title: Text('本地备份', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle( fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)), onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); }, ), Divider(height: 0.5, color: colors.outlineVariant), ListTile( contentPadding: EdgeInsets.zero, leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.cloud_outlined, color: colors.onSurface.withValues(alpha: 0.6))), title: Text('云备份', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle( fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)), onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); }, ), const SizedBox(height: 20), ], ), ), ); } }