From 674a981aac588c98398cd600013d6ffdd935c6a1 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Sat, 23 May 2026 18:14:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=EF=BC=8C=E4=BE=A7=E8=BE=B9=E6=A0=8F=EF=BC=8C=E6=88=91=E7=9A=84?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/home_page.dart | 4 +- lib/pages/profile_page.dart | 1534 +++++++--------------------- lib/pages/tag_management_page.dart | 665 +++++------- lib/widgets/custom_drawer.dart | 761 +++++--------- 4 files changed, 893 insertions(+), 2071 deletions(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ae4b60c..887a3cf 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -28,8 +28,8 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - drawer: context.watch().bottomNavIndex == 0 - ? CustomDrawer() + drawer: context.watch().bottomNavIndex != 1 + ? const CustomDrawer() : null, body: Consumer( diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index 985e175..0a03baa 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -4,8 +4,6 @@ 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 'package:package_info_plus/package_info_plus.dart'; -// import 'package:url_launcher/url_launcher.dart'; // 改为应用内打开 import 'package:webview_flutter/webview_flutter.dart'; import '../providers/app_provider.dart'; import '../utils/user_prefs.dart'; @@ -15,8 +13,10 @@ import 'sync/backup_page.dart'; import 'statistics_page.dart'; import 'sync/cloud_sync_page.dart'; import 'app_icon_picker_page.dart'; +import 'tag_management_page.dart'; +import 'stroll_page.dart'; -/// 个人中心页面 - 极简主义设计 +/// 个人中心页面 class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @@ -28,426 +28,365 @@ class _ProfilePageState extends State { final ImagePicker _picker = ImagePicker(); final UserPrefs _userPrefs = UserPrefs(); - // 用户数据 String _nickname = 'Mook'; String _motto = '好运不会眷顾一无所有之人。'; String? _avatarPath; - String _version = '0.1.5'; - bool _isLoading = true; @override void initState() { super.initState(); _loadUserData(); - _loadVersionInfo(); } - /// 加载版本信息 - Future _loadVersionInfo() async { - final packageInfo = await PackageInfo.fromPlatform(); - setState(() { - _version = packageInfo.version; - }); - } - - /// 加载用户数据 Future _loadUserData() async { - setState(() => _isLoading = true); try { await UserPrefs.init(); setState(() { _nickname = _userPrefs.nickname; _motto = _userPrefs.motto; _avatarPath = _userPrefs.avatarPath; - _isLoading = false; }); - } catch (e) { - setState(() => _isLoading = false); - } + } catch (_) {} } @override Widget build(BuildContext context) { - if (_isLoading) { - return const Center( - child: CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFF1A1A1A), - ), - ); - } - return Column( children: [ - // 标题栏 AppBar( - title: const Text('我的'), - actions: [ - IconButton( - icon: const Icon(Icons.settings_outlined), - onPressed: () => _showSettings(context), + leading: Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu, color: Color(0xFF1A1A1A)), + onPressed: () => Scaffold.of(context).openDrawer(), ), - ], + ), + title: const Text('我的'), ), - - // 内容 Expanded( - child: SingleChildScrollView( + child: Container( + color: const Color(0xFFF8F8F8), + child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 顶部用户信息 - _buildUserHeader(), - + _buildUserCard(), const SizedBox(height: 8), - - // 数据统计 - _buildStatsSection(), - - const SizedBox(height: 8), - - // 功能菜单 - _buildMenuSection(), - - const SizedBox(height: 40), - - // 底部留白,避免被 dock 栏遮挡 - const SizedBox(height: 100), + _buildExploreRow(), + const SizedBox(height: 20), + _buildSectionHeader('数据'), + _buildMenuGroup([ + _MenuEntry(Icons.analytics_outlined, '数据统计', () => _push(context, const StatisticsPage())), + _MenuEntry(Icons.backup_outlined, '备份', () => _showBackupOptions(context)), + _MenuEntry(Icons.delete_outline, '回收站', () => _push(context, const RecycleBinPage())), + ]), + const SizedBox(height: 80), ], ), ), ), + ), ], ); } - /// 用户头部信息 - Widget _buildUserHeader() { - return Container( - padding: const EdgeInsets.all(24), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 左侧头像 - GestureDetector( - onTap: _pickAvatar, - child: Container( - width: 72, - height: 72, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), - shape: BoxShape.circle, - ), - child: _avatarPath != null && _avatarPath!.isNotEmpty - ? ClipOval( - child: Image.file( - File(_avatarPath!), - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildAvatarPlaceholder(), - ), - ) - : _buildAvatarPlaceholder(), - ), - ), - - const SizedBox(width: 20), - - // 右侧信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // 昵称 - GestureDetector( - onTap: () => _editNickname(context), - child: Text( - _nickname, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), - ), - - const SizedBox(height: 6), - - // 座右铭 - GestureDetector( - onTap: () => _editMotto(context), - child: Text( - _motto, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF666666), - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ], - ), - ); + void _push(BuildContext context, Widget page) { + Navigator.push(context, MaterialPageRoute(builder: (_) => page)); } - Widget _buildAvatarPlaceholder() { - return const Center( - child: Icon( - Icons.person_outline, - size: 32, - color: Color(0xFFCCCCCC), - ), - ); - } + // ─── 用户卡片 ──────────────────────────────────────────────────────── - /// 数据统计区域 - Widget _buildStatsSection() { + Widget _buildUserCard() { return Consumer( builder: (context, provider, child) { - final movies = provider.movies; - final books = provider.books; - final notes = provider.notes; - - final movieCount = movies.where((m) => !m.isDeleted).length; - final bookCount = books.length; - final noteCount = notes.length; + final movieCount = provider.movies.where((m) => !m.isDeleted).length; + final bookCount = provider.books.length; + final noteCount = provider.notes.length; - return Padding( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), - child: Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildStatCard('观影', movieCount, '场', Icons.movie_outlined), - _buildStatCard('阅读', bookCount, '本', Icons.menu_book_outlined), - _buildStatCard('笔记', noteCount, '条', Icons.note_outlined), - ], - ), + return Container( + margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: Column( + children: [ + Row( + children: [ + GestureDetector( + onTap: _pickAvatar, + child: Container( + width: 64, height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5), + ), + clipBehavior: Clip.antiAlias, + child: _avatarPath != null && _avatarPath!.isNotEmpty + ? Image.file(File(_avatarPath!), fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC))) + : const Icon(Icons.person_outline, size: 32, color: Color(0xFFCCCCCC)), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () => _editNickname(context), + child: Text(_nickname, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), + ), + const SizedBox(height: 4), + GestureDetector( + onTap: () => _editMotto(context), + child: Text(_motto, maxLines: 1, overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.settings_outlined, color: Color(0xFF999999)), + onPressed: () => _showSettings(context), + ), + ], + ), + const SizedBox(height: 16), + const Divider(height: 1, color: Color(0xFFF0F0F0)), + const SizedBox(height: 12), + _buildStatRow(Icons.movie_outlined, _formatCount(movieCount), '观影'), + const SizedBox(height: 8), + _buildStatRow(Icons.menu_book_outlined, _formatCount(bookCount), '阅读'), + const SizedBox(height: 8), + _buildStatRow(Icons.note_outlined, _formatCount(noteCount), '笔记'), + ], ), ); }, ); } - /// 统计卡片 - Widget _buildStatCard(String label, int count, String unit, IconData icon) { - return Column( + Widget _buildStatRow(IconData icon, String count, String label) { + return Row( children: [ - Icon( - icon, - size: 24, - color: const Color(0xFF666666), - ), - const SizedBox(height: 8), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text( - '$count', - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(width: 2), - Text( - unit, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF666666), - ), - ), - ], - ), - const SizedBox(height: 2), - Text( - label, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF999999), - ), - ), + Icon(icon, size: 14, color: const Color(0xFFBBBBBB)), + const SizedBox(width: 8), + Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), + const Spacer(), + Text(count, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ], ); } - /// 功能菜单 - Widget _buildMenuSection() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 24), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - _buildMenuItem( - icon: Icons.analytics_outlined, - title: '数据统计', - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const StatisticsPage()), - ); - }, - ), - const Divider(height: 1, indent: 72, endIndent: 16, color: Color(0xFFE8E8E8)), - - _buildMenuItem( - icon: Icons.backup_outlined, - title: '备份', - onTap: () => _showBackupOptions(context), - ), - const Divider(height: 1, indent: 72, endIndent: 16, color: Color(0xFFE8E8E8)), + String _formatCount(int count) { + if (count >= 10000) return '${(count / 10000).toStringAsFixed(1)}万'; + if (count >= 1000) return '${(count / 1000).toStringAsFixed(1)}k'; + return count.toString(); + } - _buildMenuItem( - icon: Icons.delete_outline, - title: '回收站', - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const RecycleBinPage()), - ); - }, - ), + // ─── 探索行 ────────────────────────────────────────────────────────── + + Widget _buildExploreRow() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: Row( + children: [ + const Icon(Icons.auto_awesome, size: 16, color: Color(0xFFBBBBBB)), + const SizedBox(width: 10), + const Text('快捷入口', style: TextStyle(fontSize: 13, color: Color(0xFF888888))), + const Spacer(), + _buildQuickAction(Icons.explore_outlined, '漫步', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage()))), + const SizedBox(width: 24), + _buildQuickAction(Icons.label_outline, '标签', () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TagManagementPage()))), ], ), ); } - /// 菜单项 - Widget _buildMenuItem({ - required IconData icon, - required String title, - required VoidCallback onTap, - }) { - return InkWell( + Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) { + return GestureDetector( onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Row( - children: [ - // 图标背景 - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - child: Icon( - icon, - size: 20, - color: const Color(0xFF666666), - ), - ), - const SizedBox(width: 16), - // 标题 - Expanded( - child: Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), - ), - // 箭头 - const Icon( - Icons.chevron_right, - size: 20, - color: Color(0xFFCCCCCC), - ), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 20, color: const Color(0xFF555555)), + const SizedBox(height: 2), + Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))), + ], ), ); } - /// 显示备份选项 + // ─── 菜单 ──────────────────────────────────────────────────────────── + + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), + child: Text(title, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFBBBBBB))), + ); + } + + Widget _buildMenuGroup(List<_MenuEntry> entries) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: Column( + children: entries.asMap().entries.map((e) { + final isLast = e.key == entries.length - 1; + return Column( + children: [ + InkWell( + onTap: e.value.onTap, + borderRadius: BorderRadius.only( + topLeft: e.key == 0 ? const Radius.circular(16) : Radius.zero, + topRight: e.key == 0 ? const Radius.circular(16) : Radius.zero, + bottomLeft: isLast ? const Radius.circular(16) : Radius.zero, + bottomRight: isLast ? const Radius.circular(16) : Radius.zero, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), + child: Row( + children: [ + Container( + width: 36, height: 36, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(e.value.icon, size: 18, color: const Color(0xFF666666)), + ), + const SizedBox(width: 12), + Expanded(child: Text(e.value.title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))), + const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)), + ], + ), + ), + ), + if (!isLast) const Divider(height: 1, indent: 68, endIndent: 20, color: Color(0xFFF0F0F0)), + ], + ); + }).toList(), + ), + ); + } + + // ─── 头像 ──────────────────────────────────────────────────────────── + + Future _pickAvatar() 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 _editNickname(BuildContext context) { + final controller = TextEditingController(text: _nickname); + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.white, elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + title: const Text('修改昵称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), + content: TextField(controller: controller, decoration: const InputDecoration(hintText: '输入昵称', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))), + TextButton(onPressed: () async { + final newNickname = controller.text.trim(); + if (newNickname.isNotEmpty) { + await _userPrefs.setNickname(newNickname); + setState(() => _nickname = newNickname); + } + Navigator.pop(context); + }, child: const Text('确定')), + ], + ), + ); + } + + void _editMotto(BuildContext context) { + final controller = TextEditingController(text: _motto); + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.white, elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + title: const Text('修改座右铭', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), + content: TextField(controller: controller, maxLines: 2, decoration: const InputDecoration(hintText: '输入座右铭', border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xFFE5E5E5))))), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))), + TextButton(onPressed: () async { + final newMotto = controller.text.trim(); + await _userPrefs.setMotto(newMotto); + setState(() => _motto = newMotto); + Navigator.pop(context); + }, child: const Text('确定')), + ], + ), + ); + } + + // ─── 备份弹窗 ──────────────────────────────────────────────────────── + void _showBackupOptions(BuildContext context) { showModalBottomSheet( context: context, backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), + 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: const Color(0xFFDDDDDD), - borderRadius: BorderRadius.circular(2), - ), - ), + Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))), const SizedBox(height: 20), - const Align( - alignment: Alignment.centerLeft, - child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), - ), + const Align(alignment: Alignment.centerLeft, child: Text('选择备份方式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))), const SizedBox(height: 16), ListTile( contentPadding: EdgeInsets.zero, - leading: Container( - width: 44, height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon(Icons.folder_outlined, color: Color(0xFF666666)), - ), + leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.folder_outlined, color: Color(0xFF666666))), title: const Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), subtitle: const Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)), - onTap: () { - Navigator.pop(ctx); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const BackupPage()), - ).then((_) => _loadUserData()); - }, + onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); }, ), const Divider(height: 0.5, color: Color(0xFFF0F0F0)), ListTile( contentPadding: EdgeInsets.zero, - leading: Container( - width: 44, height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon(Icons.cloud_outlined, color: Color(0xFF666666)), - ), + leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.cloud_outlined, color: Color(0xFF666666))), title: const Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), subtitle: const Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), trailing: const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC)), - onTap: () { - Navigator.pop(ctx); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const CloudSyncPage()), - ); - }, + onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); }, ), const SizedBox(height: 20), ], @@ -456,150 +395,22 @@ class _ProfilePageState extends State { ); } - /// 显示提示 - void _showToast(String message) { - ToastUtil.show(context, message); - } - - /// 选择头像 - Future _pickAvatar() async { - try { - final XFile? 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) { - _showToast('选择头像失败: $e'); - } - } - } - void _editNickname(BuildContext context) { - final controller = TextEditingController(text: _nickname); - - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Colors.white, - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - title: const Text( - '修改昵称', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), - content: TextField( - controller: controller, - decoration: const InputDecoration( - hintText: '输入昵称', - border: UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xFFE5E5E5)), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), - ), - TextButton( - onPressed: () async { - final newNickname = controller.text.trim(); - if (newNickname.isNotEmpty) { - await _userPrefs.setNickname(newNickname); - setState(() => _nickname = newNickname); - } - Navigator.pop(context); - }, - child: const Text('确定'), - ), - ], - ), - ); - } - - /// 编辑座右铭 - void _editMotto(BuildContext context) { - final controller = TextEditingController(text: _motto); - - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Colors.white, - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - title: const Text( - '修改座右铭', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), - content: TextField( - controller: controller, - maxLines: 2, - decoration: const InputDecoration( - hintText: '输入座右铭', - border: UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xFFE5E5E5)), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), - ), - TextButton( - onPressed: () async { - final newMotto = controller.text.trim(); - await _userPrefs.setMotto(newMotto); - setState(() => _motto = newMotto); - Navigator.pop(context); - }, - child: const Text('确定'), - ), - ], - ), - ); - } - - /// 显示设置 void _showSettings(BuildContext context) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsPage(), - ), - ); + _push(context, const SettingsPage()); } } -/// 设置页面 +// ─── 数据类 ──────────────────────────────────────────────────────────── + +class _MenuEntry { + final IconData icon; + final String title; + final VoidCallback onTap; + _MenuEntry(this.icon, this.title, this.onTap); +} + +// ─── 设置页面 ────────────────────────────────────────────────────────── + class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -621,12 +432,9 @@ class _SettingsPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - appBar: AppBar( - title: const Text('设置'), - ), + appBar: AppBar(title: const Text('设置')), body: ListView( children: [ - // 数据管理 _buildSectionHeader('数据管理'), _buildActionItem( icon: Icons.cleaning_services_outlined, @@ -635,9 +443,6 @@ class _SettingsPageState extends State { onTap: () => _showClearCacheDialog(context), ), const Divider(height: 0.5, indent: 24, endIndent: 24), - - // 主界面功能显示入口 - _buildSectionHeader('个性化设置'), _buildSwitchItem( icon: Icons.swipe_vertical_outlined, title: '底部导航栏滚动隐藏', @@ -650,47 +455,23 @@ class _SettingsPageState extends State { icon: Icons.apps_outlined, title: '应用图标', subtitle: '更换桌面应用图标', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AppIconPickerPage(), - ), - ); - }, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AppIconPickerPage())), ), const Divider(height: 0.5, indent: 24, endIndent: 24), _buildNavigationItem( icon: Icons.view_list_outlined, title: '主界面设置', subtitle: '启动标签、模块显示开关', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const MainContentSettingsPage(), - ), - ); - }, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MainContentSettingsPage())), ), const Divider(height: 0.5, indent: 24, endIndent: 24), - - // 布局设置 - _buildSectionHeader('布局设置'), _buildNavigationItem( icon: Icons.dashboard_outlined, title: '布局设置', subtitle: '笔记、影视、阅读的展示样式', - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const LayoutSettingsPage()), - ); - }, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())), ), const Divider(height: 0.5, indent: 24, endIndent: 24), - - // 使用说明 _buildSectionHeader('帮助'), _buildLinkItem( context: context, @@ -700,287 +481,107 @@ class _SettingsPageState extends State { url: 'https://mooknote.iletter.top/#/guide', ), const Divider(height: 0.5, indent: 24, endIndent: 24), - - // 关于作者 - // _buildLinkItem( - // context: context, - // icon: Icons.person_outline, - // title: '关于作者', - // subtitle: '了解更多信息', - // url: 'https://www.iletter.top/', - // ), - // const Divider(height: 0.5, indent: 24, endIndent: 24), ], ), ); } - /// 切换底部导航栏滚动隐藏 Future _toggleHideBottomNavOnScroll(bool value) async { await _userPrefs.setHideBottomNavOnScroll(value); setState(() => _hideBottomNavOnScroll = value); } - /// 构建开关项 - Widget _buildSwitchItem({ - required IconData icon, - required String title, - required String subtitle, - required bool value, - required ValueChanged onChanged, - }) { + Widget _buildSwitchItem({required IconData icon, required String title, required String subtitle, required bool value, required ValueChanged onChanged}) { return InkWell( onTap: () => onChanged(!value), child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Row( children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - color: const Color(0xFF666666), - size: 22, - ), - ), + Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)), const SizedBox(width: 16), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF999999), - ), - ), - ], - ), - ), - Switch( - value: value, - onChanged: onChanged, - activeColor: const Color(0xFF1A1A1A), - activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), - inactiveThumbColor: Colors.white, - inactiveTrackColor: const Color(0xFFE5E5E5), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + const SizedBox(height: 2), + Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), + ]), ), + Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)), ], ), ), ); } - /// 构建区块标题 Widget _buildSectionHeader(String title) { return Padding( padding: const EdgeInsets.fromLTRB(24, 32, 24, 12), - child: Text( - title, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), + child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ); } - /// 构建导航项 - Widget _buildNavigationItem({ - required IconData icon, - required String title, - required String subtitle, - required VoidCallback onTap, - }) { + Widget _buildNavigationItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) { return InkWell( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Row( children: [ - // 图标背景 - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - color: const Color(0xFF666666), - size: 22, - ), - ), + Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)), const SizedBox(width: 16), - // 文字内容 Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF999999), - ), - ), - ], - ), - ), - // 箭头 - const Icon( - Icons.chevron_right, - color: Color(0xFFCCCCCC), - size: 20, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + const SizedBox(height: 2), + Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), + ]), ), + const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20), ], ), ), ); } - /// 构建链接项 - Widget _buildLinkItem({ - required BuildContext context, - required IconData icon, - required String title, - required String subtitle, - required String url, - }) { + Widget _buildLinkItem({required BuildContext context, required IconData icon, required String title, required String subtitle, required String url}) { return InkWell( onTap: () => _launchUrl(context, url), child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Row( children: [ - // 图标背景 - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - color: const Color(0xFF666666), - size: 22, - ), - ), + Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)), const SizedBox(width: 16), - // 文字内容 Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF999999), - ), - ), - ], - ), - ), - // 外部链接图标 - const Icon( - Icons.open_in_new, - color: Color(0xFFCCCCCC), - size: 18, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + const SizedBox(height: 2), + Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), + ]), ), + const Icon(Icons.open_in_new, color: Color(0xFFCCCCCC), size: 18), ], ), ), ); } - /// 构建操作项(无箭头) - Widget _buildActionItem({ - required IconData icon, - required String title, - required String subtitle, - required VoidCallback onTap, - }) { + Widget _buildActionItem({required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) { return InkWell( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Row( children: [ - // 图标背景 - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - color: const Color(0xFF666666), - size: 22, - ), - ), + Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)), const SizedBox(width: 16), - // 文字内容 Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF999999), - ), - ), - ], - ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + const SizedBox(height: 2), + Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), + ]), ), ], ), @@ -988,162 +589,72 @@ class _SettingsPageState extends State { ); } - /// 显示清除缓存对话框 void _showClearCacheDialog(BuildContext pageContext) { showDialog( context: pageContext, builder: (dialogContext) => AlertDialog( - backgroundColor: Colors.white, - elevation: 0, + backgroundColor: Colors.white, elevation: 0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), title: const Text('清除缓存数据'), content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), - ), - TextButton( - onPressed: () async { - Navigator.pop(dialogContext); - await _clearCacheData(pageContext); - }, - child: const Text('确定', style: TextStyle(color: Colors.red)), - ), + TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('取消', style: TextStyle(color: Color(0xFF666666)))), + TextButton(onPressed: () async { + Navigator.pop(dialogContext); + await _clearCacheData(pageContext); + }, child: const Text('确定', style: TextStyle(color: Colors.red))), ], ), ); } - /// 清除缓存数据 Future _clearCacheData(BuildContext context) async { try { - // 显示进度提示 - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => const Center( - child: CircularProgressIndicator(), - ), - ); - - // 获取所有数据库中的图片路径 + showDialog(context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator())); final appProvider = context.read(); final dbImagePaths = await _getAllDbImagePaths(appProvider); - - // 清理图片目录 final deletedCount = await _cleanImageDirectory(dbImagePaths); - - // 关闭进度提示 Navigator.pop(context); - - // 显示结果 - if (context.mounted) { - ToastUtil.show(context, '已清理 $deletedCount 个缓存文件'); - } + if (context.mounted) ToastUtil.show(context, '已清理 $deletedCount 个缓存文件'); } catch (e) { - // 关闭进度提示 Navigator.pop(context); - if (context.mounted) { - ToastUtil.show(context, '清理失败: $e'); - } + if (context.mounted) ToastUtil.show(context, '清理失败: $e'); } } - /// 获取数据库中所有图片路径 Future> _getAllDbImagePaths(AppProvider provider) async { final paths = {}; - - // 获取所有影视的封面路径 - final movies = provider.movies; - for (final movie in movies) { - final posterPath = movie.posterPath; - if (posterPath != null && posterPath.isNotEmpty) { - paths.add(posterPath); - } + for (final movie in provider.movies) { if (movie.posterPath?.isNotEmpty == true) paths.add(movie.posterPath!); } + for (final book in provider.books) { if (book.coverPath?.isNotEmpty == true) paths.add(book.coverPath!); } + for (final note in provider.notes) { for (final p in note.images) { if (p.isNotEmpty) paths.add(p); } } + for (final movieId in provider.movies.map((m) => m.id)) { + for (final poster in await provider.getMoviePosters(movieId)) { if (poster.posterPath.isNotEmpty) paths.add(poster.posterPath); } } - - // 获取所有书籍的封面路径 - final books = provider.books; - for (final book in books) { - final coverPath = book.coverPath; - if (coverPath != null && coverPath.isNotEmpty) { - paths.add(coverPath); - } - } - - // 获取所有笔记中的图片路径 - final notes = provider.notes; - for (final note in notes) { - for (final imagePath in note.images) { - if (imagePath.isNotEmpty) { - paths.add(imagePath); - } - } - } - - // 获取所有海报墙图片路径 - final movieIds = movies.map((m) => m.id).toList(); - for (final movieId in movieIds) { - final posters = await provider.getMoviePosters(movieId); - for (final poster in posters) { - final posterPath = poster.posterPath; - if (posterPath.isNotEmpty) { - paths.add(posterPath); - } - } - } - return paths; } - /// 清理图片目录 Future _cleanImageDirectory(Set dbImagePaths) async { int deletedCount = 0; - try { - // 获取应用文档目录 final appDir = await getApplicationDocumentsDirectory(); final imagesDir = Directory('${appDir.path}/images'); - - if (!await imagesDir.exists()) { - return 0; - } - - // 递归遍历所有文件 + if (!await imagesDir.exists()) return 0; await for (final entity in imagesDir.list(recursive: true, followLinks: false)) { - if (entity is File) { - final filePath = entity.path; - // 如果文件不在数据库中,删除它 - if (!dbImagePaths.contains(filePath)) { - try { - await entity.delete(); - deletedCount++; - } catch (e) { - // 忽略单个文件删除错误 - } - } + if (entity is File && !dbImagePaths.contains(entity.path)) { + try { await entity.delete(); deletedCount++; } catch (_) {} } } - } catch (e) { - debugPrint('清理图片目录失败: $e'); - } - + } catch (e) { debugPrint('清理图片目录失败: $e'); } return deletedCount; } - /// 打开链接(应用内打开) void _launchUrl(BuildContext context, String url) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => WebViewPage(url: url), - ), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => WebViewPage(url: url))); } } -/// 主界面功能显示设置页面 +// ─── 主界面设置 ──────────────────────────────────────────────────────── + class MainContentSettingsPage extends StatefulWidget { const MainContentSettingsPage({super.key}); @@ -1153,7 +664,6 @@ class MainContentSettingsPage extends StatefulWidget { class _MainContentSettingsPageState extends State { final UserPrefs _userPrefs = UserPrefs(); - bool _showMovieTab = true; bool _showBookTab = true; bool _showNoteTab = true; @@ -1165,7 +675,6 @@ class _MainContentSettingsPageState extends State { _loadSettings(); } - /// 加载设置 void _loadSettings() { setState(() { _showMovieTab = _userPrefs.showMovieTab; @@ -1175,7 +684,6 @@ class _MainContentSettingsPageState extends State { }); } - /// 获取已启用的标签数量 int get _enabledTabCount { int count = 0; if (_showMovieTab) count++; @@ -1184,139 +692,80 @@ class _MainContentSettingsPageState extends State { return count; } - /// 切换观影标签显示 Future _toggleMovieTab(bool value) async { - if (!value && _enabledTabCount <= 1) { - _showToast('至少保留一个标签页'); - return; - } + if (!value && _enabledTabCount <= 1) { ToastUtil.show(context, '至少保留一个标签页'); return; } await _userPrefs.setShowMovieTab(value); setState(() => _showMovieTab = value); } - /// 切换阅读标签显示 Future _toggleBookTab(bool value) async { - if (!value && _enabledTabCount <= 1) { - _showToast('至少保留一个标签页'); - return; - } + if (!value && _enabledTabCount <= 1) { ToastUtil.show(context, '至少保留一个标签页'); return; } await _userPrefs.setShowBookTab(value); setState(() => _showBookTab = value); } - /// 切换笔记标签显示 Future _toggleNoteTab(bool value) async { - if (!value && _enabledTabCount <= 1) { - _showToast('至少保留一个标签页'); - return; - } + if (!value && _enabledTabCount <= 1) { ToastUtil.show(context, '至少保留一个标签页'); return; } await _userPrefs.setShowNoteTab(value); setState(() => _showNoteTab = value); } - /// 显示提示 - void _showToast(String message) { - ToastUtil.show(context, message); - } - @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - appBar: AppBar( - title: const Text('主界面设置'), - ), + appBar: AppBar(title: const Text('主界面设置')), body: ListView( children: [ - // 启动设置 _buildSectionHeader('启动设置'), _buildDefaultTabSelector(), const Divider(height: 0.5, indent: 24, endIndent: 24), - - // 模块开关 _buildSectionHeader('模块开关'), - _buildSwitchItem( - icon: Icons.movie_outlined, - title: '观影', - subtitle: '记录和管理观影记录', - value: _showMovieTab, - onChanged: _toggleMovieTab, - ), + _buildSwitchItem(Icons.movie_outlined, '观影', '记录和管理观影记录', _showMovieTab, _toggleMovieTab), const Divider(height: 0.5, indent: 24, endIndent: 24), - _buildSwitchItem( - icon: Icons.menu_book_outlined, - title: '阅读', - subtitle: '记录和管理阅读记录', - value: _showBookTab, - onChanged: _toggleBookTab, - ), + _buildSwitchItem(Icons.menu_book_outlined, '阅读', '记录和管理阅读记录', _showBookTab, _toggleBookTab), const Divider(height: 0.5, indent: 24, endIndent: 24), - _buildSwitchItem( - icon: Icons.note_outlined, - title: '笔记', - subtitle: '记录和管理笔记', - value: _showNoteTab, - onChanged: _toggleNoteTab, - ), + _buildSwitchItem(Icons.note_outlined, '笔记', '记录和管理笔记', _showNoteTab, _toggleNoteTab), const Divider(height: 0.5, indent: 24, endIndent: 24), Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: const Text( - '至少保留一个模块,关闭后对应标签页将不再显示。', - style: TextStyle(fontSize: 12, color: Color(0xFFBBBBBB)), - ), + child: const Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))), ), ], ), ); } - /// 构建区块标题 Widget _buildSectionHeader(String title) { return Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), - child: Text( - title, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), + child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), + ); + } + + Widget _buildSwitchItem(IconData icon, String title, String subtitle, bool value, ValueChanged onChanged) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), + leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)), + title: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), + trailing: Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF1A1A1A), activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), inactiveThumbColor: Colors.white, inactiveTrackColor: const Color(0xFFE5E5E5)), ); } - /// 构建默认启动标签选择器 Widget _buildDefaultTabSelector() { final labels = ['影视', '阅读', '笔记']; final icons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined]; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - leading: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon(Icons.home_outlined, color: Color(0xFF666666), size: 22), - ), - title: const Text( - '默认启动标签', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)), - ), - subtitle: const Text( - '打开应用时默认显示的页面', - style: TextStyle(fontSize: 12, color: Color(0xFF999999)), - ), + leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.home_outlined, color: Color(0xFF666666), size: 22)), + title: const Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + subtitle: const Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - labels[_defaultTabIndex], - style: const TextStyle(fontSize: 14, color: Color(0xFF999999)), - ), + Text(labels[_defaultTabIndex], style: const TextStyle(fontSize: 14, color: Color(0xFF999999))), const SizedBox(width: 4), const Icon(Icons.chevron_right, color: Color(0xFFCCCCCC), size: 20), ], @@ -1330,44 +779,21 @@ class _MainContentSettingsPageState extends State { context: context, backgroundColor: Colors.transparent, builder: (ctx) => Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), + decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16))), padding: const EdgeInsets.symmetric(vertical: 12), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 36, height: 4, - decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2)), - ), + Container(width: 36, height: 4, decoration: BoxDecoration(color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(2))), const SizedBox(height: 20), - const Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), - ), - ), + const Align(alignment: Alignment.centerLeft, child: Padding(padding: EdgeInsets.symmetric(horizontal: 24), child: Text('默认启动标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))))), const SizedBox(height: 16), for (int i = 0; i < labels.length; i++) ListTile( - leading: Container( - width: 44, height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: Icon(icons[i], color: const Color(0xFF666666)), - ), + leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: const Color(0xFF666666))), title: Text(labels[i], style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), trailing: _defaultTabIndex == i ? const Icon(Icons.check, color: Color(0xFF1A1A1A), size: 20) : null, - onTap: () async { - await _userPrefs.setDefaultMainTabIndex(i); - setState(() => _defaultTabIndex = i); - Navigator.pop(ctx); - }, + onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); }, ), const SizedBox(height: 8), ], @@ -1375,58 +801,10 @@ class _MainContentSettingsPageState extends State { ), ); } - - /// 构建开关项 - Widget _buildSwitchItem({ - required IconData icon, - required String title, - required String subtitle, - required bool value, - required ValueChanged onChanged, - }) { - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - leading: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - icon, - color: const Color(0xFF666666), - size: 24, - ), - ), - title: Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), - subtitle: Text( - subtitle, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), - trailing: Switch( - value: value, - onChanged: onChanged, - activeColor: const Color(0xFF1A1A1A), - activeTrackColor: const Color(0xFF1A1A1A).withOpacity(0.3), - inactiveThumbColor: Colors.white, - inactiveTrackColor: const Color(0xFFE5E5E5), - ), - ); - } } -/// 布局设置页面 +// ─── 布局设置 ────────────────────────────────────────────────────────── + class LayoutSettingsPage extends StatefulWidget { const LayoutSettingsPage({super.key}); @@ -1452,81 +830,25 @@ class _LayoutSettingsPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - appBar: AppBar( - title: const Text('布局设置'), - ), + appBar: AppBar(title: const Text('布局设置')), body: ListView( children: [ - // 笔记布局 _buildSectionHeader('笔记布局'), - _buildLayoutOption( - icon: Icons.view_list_outlined, - title: '列表布局', - subtitle: '单列列表,简洁清晰', - value: 0, - groupValue: _noteLayout, - onTap: () => _setLayout('note', 0), - ), + _buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列列表,简洁清晰', value: 0, groupValue: _noteLayout, onTap: () => _setLayout('note', 0)), const Divider(height: 0.5, indent: 24, endIndent: 24), - _buildLayoutOption( - icon: Icons.grid_view_outlined, - title: '瀑布流布局', - subtitle: '双列卡片,图文并茂', - value: 1, - groupValue: _noteLayout, - onTap: () => _setLayout('note', 1), - ), + _buildLayoutOption(icon: Icons.grid_view_outlined, title: '瀑布流布局', subtitle: '双列卡片,图文并茂', value: 1, groupValue: _noteLayout, onTap: () => _setLayout('note', 1)), const Divider(height: 0.5, indent: 24, endIndent: 24), - _buildLayoutOption( - icon: Icons.timeline_outlined, - title: '时间线布局', - subtitle: '按时间排列,纵览全局', - value: 2, - groupValue: _noteLayout, - onTap: () => _setLayout('note', 2), - ), + _buildLayoutOption(icon: Icons.timeline_outlined, title: '时间线布局', subtitle: '按时间排列,纵览全局', value: 2, groupValue: _noteLayout, onTap: () => _setLayout('note', 2)), const Divider(height: 0.5, indent: 24, endIndent: 24), - - // 影视布局 _buildSectionHeader('影视布局'), - _buildLayoutOption( - icon: Icons.grid_view_outlined, - title: '海报网格', - subtitle: '三列海报,赏心悦目', - value: 0, - groupValue: _movieLayout, - onTap: () => _setLayout('movie', 0), - ), + _buildLayoutOption(icon: Icons.grid_view_outlined, title: '海报网格', subtitle: '三列海报,赏心悦目', value: 0, groupValue: _movieLayout, onTap: () => _setLayout('movie', 0)), const Divider(height: 0.5, indent: 24, endIndent: 24), - _buildLayoutOption( - icon: Icons.view_list_outlined, - title: '列表布局', - subtitle: '单列卡片,信息一览', - value: 1, - groupValue: _movieLayout, - onTap: () => _setLayout('movie', 1), - ), + _buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _movieLayout, onTap: () => _setLayout('movie', 1)), const Divider(height: 0.5, indent: 24, endIndent: 24), - - // 阅读布局 _buildSectionHeader('阅读布局'), - _buildLayoutOption( - icon: Icons.grid_view_outlined, - title: '封面网格', - subtitle: '三列封面,清新雅致', - value: 0, - groupValue: _bookLayout, - onTap: () => _setLayout('book', 0), - ), + _buildLayoutOption(icon: Icons.grid_view_outlined, title: '封面网格', subtitle: '三列封面,清新雅致', value: 0, groupValue: _bookLayout, onTap: () => _setLayout('book', 0)), const Divider(height: 0.5, indent: 24, endIndent: 24), - _buildLayoutOption( - icon: Icons.view_list_outlined, - title: '列表布局', - subtitle: '单列卡片,信息一览', - value: 1, - groupValue: _bookLayout, - onTap: () => _setLayout('book', 1), - ), + _buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _bookLayout, onTap: () => _setLayout('book', 1)), const Divider(height: 0.5, indent: 24, endIndent: 24), ], ), @@ -1535,62 +857,27 @@ class _LayoutSettingsPageState extends State { void _setLayout(String type, int value) async { switch (type) { - case 'note': - await _userPrefs.setNoteLayoutStyle(value); - setState(() => _noteLayout = value); - break; - case 'movie': - await _userPrefs.setMovieLayoutStyle(value); - setState(() => _movieLayout = value); - break; - case 'book': - await _userPrefs.setBookLayoutStyle(value); - setState(() => _bookLayout = value); - break; + case 'note': await _userPrefs.setNoteLayoutStyle(value); setState(() => _noteLayout = value); + case 'movie': await _userPrefs.setMovieLayoutStyle(value); setState(() => _movieLayout = value); + case 'book': await _userPrefs.setBookLayoutStyle(value); setState(() => _bookLayout = value); } } Widget _buildSectionHeader(String title) { return Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), - child: Text( - title, - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), - ), + child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ); } - Widget _buildLayoutOption({ - required IconData icon, - required String title, - required String subtitle, - required int value, - required int groupValue, - required VoidCallback onTap, - }) { + Widget _buildLayoutOption({required IconData icon, required String title, required String subtitle, required int value, required int groupValue, required VoidCallback onTap}) { final selected = value == groupValue; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - leading: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(10), - ), - child: Icon(icon, color: const Color(0xFF666666), size: 22), - ), - title: Text( - title, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)), - ), - subtitle: Text( - subtitle, - style: const TextStyle(fontSize: 12, color: Color(0xFF999999)), - ), - trailing: selected - ? const Icon(Icons.check_circle, color: Color(0xFF1A1A1A), size: 20) - : const Icon(Icons.circle_outlined, color: Color(0xFFDDDDDD), size: 20), + leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: const Color(0xFF666666), size: 22)), + title: Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + subtitle: Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), + trailing: selected ? const Icon(Icons.check_circle, color: Color(0xFF1A1A1A), size: 20) : const Icon(Icons.circle_outlined, color: Color(0xFFDDDDDD), size: 20), onTap: onTap, ); } @@ -1599,7 +886,6 @@ class _LayoutSettingsPageState extends State { /// WebView 页面 class WebViewPage extends StatefulWidget { final String url; - const WebViewPage({super.key, required this.url}); @override @@ -1615,25 +901,11 @@ class _WebViewPageState extends State { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate( - onPageStarted: (String url) { - setState(() { - _isLoading = true; - }); - }, - onPageFinished: (String url) { - setState(() { - _isLoading = false; - }); - }, - onWebResourceError: (WebResourceError error) { - setState(() { - _isLoading = false; - }); - }, - ), - ) + ..setNavigationDelegate(NavigationDelegate( + onPageStarted: (_) => setState(() => _isLoading = true), + onPageFinished: (_) => setState(() => _isLoading = false), + onWebResourceError: (_) => setState(() => _isLoading = false), + )) ..loadRequest(Uri.parse(widget.url)); } @@ -1641,26 +913,8 @@ class _WebViewPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - appBar: AppBar( - title: const Text(''), - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => _controller.reload(), - ), - ], - ), - body: Stack( - children: [ - WebViewWidget(controller: _controller), - if (_isLoading) - const Center( - child: CircularProgressIndicator( - color: Color(0xFF999999), - ), - ), - ], - ), + appBar: AppBar(title: const Text(''), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: () => _controller.reload())]), + body: Stack(children: [WebViewWidget(controller: _controller), if (_isLoading) const Center(child: CircularProgressIndicator(color: Color(0xFF999999)))]), ); } } diff --git a/lib/pages/tag_management_page.dart b/lib/pages/tag_management_page.dart index 265b907..c6f2215 100644 --- a/lib/pages/tag_management_page.dart +++ b/lib/pages/tag_management_page.dart @@ -16,6 +16,7 @@ class _TagManagementPageState extends State { static const _tabTypes = ['movie_genre', 'book_genre', 'note_tag']; static const _typeLabels = ['影视类型', '书籍类型', '笔记标签']; + static const _typeIcons = [Icons.movie_outlined, Icons.menu_book_outlined, Icons.note_outlined]; final Map>> _tagCache = {}; @@ -28,9 +29,7 @@ class _TagManagementPageState extends State { Future _loadTags(String type) async { final provider = context.read(); final tags = await provider.getTags(type); - if (mounted) { - setState(() => _tagCache[type] = tags); - } + if (mounted) setState(() => _tagCache[type] = tags); } Future _syncTags() async { @@ -49,24 +48,49 @@ class _TagManagementPageState extends State { String get _currentType => _tabTypes[_currentIndex]; + /// 计算标签使用次数 + Map _getTagUsageCounts(String type) { + final provider = context.read(); + final counts = {}; + + switch (type) { + case 'movie_genre': + for (final m in provider.movies.where((m) => !m.isDeleted)) { + for (final g in m.genres) { + counts[g] = (counts[g] ?? 0) + 1; + } + } + break; + case 'book_genre': + for (final b in provider.books) { + for (final g in b.genres) { + counts[g] = (counts[g] ?? 0) + 1; + } + } + break; + case 'note_tag': + for (final n in provider.notes.where((n) => !n.isDeleted)) { + for (final t in n.tags) { + counts[t] = (counts[t] ?? 0) + 1; + } + } + break; + } + return counts; + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, + backgroundColor: const Color(0xFFF8F8F8), appBar: AppBar( title: const Text('标签管理'), actions: [ _isSyncing ? const Padding( padding: EdgeInsets.all(16), - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFF1A1A1A), - ), - ), + child: SizedBox(width: 20, height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF1A1A1A))), ) : IconButton( icon: const Icon(Icons.sync, size: 20), @@ -77,12 +101,10 @@ class _TagManagementPageState extends State { ), body: Column( children: [ - const SizedBox(height: 8), + const SizedBox(height: 12), _buildTabSelector(), - const SizedBox(height: 20), - Expanded( - child: _buildTagList(_currentType), - ), + const SizedBox(height: 16), + Expanded(child: _buildTagList(_currentType)), ], ), floatingActionButton: GestureDetector( @@ -93,11 +115,7 @@ class _TagManagementPageState extends State { color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(24), boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.12), - blurRadius: 12, - offset: const Offset(0, 4), - ), + BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)), ], ), child: Row( @@ -105,14 +123,8 @@ class _TagManagementPageState extends State { children: [ const Icon(Icons.add, size: 18, color: Colors.white), const SizedBox(width: 6), - Text( - '添加${_typeLabels[_currentIndex]}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), + Text('添加${_typeLabels[_currentIndex]}', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), ], ), ), @@ -120,13 +132,14 @@ class _TagManagementPageState extends State { ); } - /// 胶囊式 Tab 选择器(水滴滑动动画) + // ─── Tab 选择器 ────────────────────────────────────────────────────── + Widget _buildTabSelector() { return Container( margin: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: const Color(0xFFF0F0F0), + color: const Color(0xFFEBEBEB), borderRadius: BorderRadius.circular(24), ), child: LayoutBuilder( @@ -136,14 +149,11 @@ class _TagManagementPageState extends State { height: 42, child: Stack( children: [ - // 水滴指示器 AnimatedPositioned( - duration: const Duration(milliseconds: 450), + duration: const Duration(milliseconds: 600), curve: Curves.elasticOut, left: _currentIndex * tabWidth, - top: 0, - bottom: 0, - width: tabWidth, + top: 0, bottom: 0, width: tabWidth, child: Padding( padding: const EdgeInsets.all(3), child: Container( @@ -151,43 +161,29 @@ class _TagManagementPageState extends State { color: Colors.white, borderRadius: BorderRadius.circular(22), boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 8, - offset: const Offset(0, 2), - ), - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 2, - offset: const Offset(0, 1), - ), + BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2)), + BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 2, offset: const Offset(0, 1)), ], ), ), ), ), - // 标签文字 Row( children: List.generate(3, (i) { final selected = _currentIndex == i; return Expanded( child: GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () { setState(() => _currentIndex = i); _loadTags(_tabTypes[i]); }, - child: Center( - child: Text( - _typeLabels[i], - style: TextStyle( - fontSize: 14, - fontWeight: - selected ? FontWeight.w600 : FontWeight.w500, - color: selected - ? const Color(0xFF1A1A1A) - : const Color(0xFF999999), - ), - ), + child: Container( + height: double.infinity, + alignment: Alignment.center, + child: Text(_typeLabels[i], + style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF999999))), ), ), ); @@ -201,89 +197,127 @@ class _TagManagementPageState extends State { ); } + // ─── 标签列表 ──────────────────────────────────────────────────────── + Widget _buildTagList(String type) { final tags = _tagCache[type] ?? []; + final usageCounts = _getTagUsageCounts(type); if (tags.isEmpty) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(16), - ), - child: const Icon(Icons.label_outline, - size: 24, color: Color(0xFFCCCCCC)), - ), - const SizedBox(height: 16), - const Text('暂无标签', - style: TextStyle( - fontSize: 14, - color: Color(0xFFBBBBBB), - fontWeight: FontWeight.w500)), - const SizedBox(height: 6), - const Text('点击下方按钮添加', - style: TextStyle(fontSize: 12, color: Color(0xFFD5D5D5))), - ], - ), - ); + return _buildEmptyState(type); } - return SingleChildScrollView( + // 按使用次数降序排序 + final sorted = List>.from(tags) + ..sort((a, b) { + final ca = usageCounts[a['name']] ?? 0; + final cb = usageCounts[b['name']] ?? 0; + return cb.compareTo(ca); + }); + + return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Wrap( - spacing: 10, - runSpacing: 10, - children: tags.map((tag) => _buildTagChip(tag)).toList(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 统计行 + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Text('共 ${tags.length} 个标签', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))), + const Spacer(), + Text('已使用 ${usageCounts.length} 个', style: const TextStyle(fontSize: 12, color: Color(0xFFD0D0D0))), + ], + ), + ), + // 标签列表 + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 80), + child: Wrap( + spacing: 10, + runSpacing: 10, + children: sorted.map((tag) => _buildTagChip(tag, usageCounts)).toList(), + ), + ), + ), + ], ), ); } - Widget _buildTagChip(Map tag) { + Widget _buildTagChip(Map tag, Map usageCounts) { final name = tag['name'] as String; + final count = usageCounts[name] ?? 0; - return GestureDetector( - onLongPress: () => _showRenameDialog(tag), - onTap: () => _showDeleteDialog(tag), - child: Container( - padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8), - decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: const Color(0xFFECECEC), width: 0.5), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A), - ), - ), + return Container( + padding: const EdgeInsets.only(left: 14, right: 6, top: 8, bottom: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 标签名区域 — 点击改名 + GestureDetector( + onTap: () => _showRenameDialog(tag), + child: Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A))), + ), + if (count > 0) ...[ const SizedBox(width: 6), Container( - width: 22, - height: 22, - decoration: BoxDecoration( - color: const Color(0xFFECECEC), - borderRadius: BorderRadius.circular(11), - ), - child: const Icon(Icons.close, - size: 12, color: Color(0xFF999999)), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)), + child: Text('$count', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Color(0xFF999999))), ), ], - ), + const SizedBox(width: 4), + // X 按钮 — 点击删除 + GestureDetector( + onTap: () => _showDeleteDialog(tag), + child: Container( + width: 22, height: 22, + decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(11)), + child: const Icon(Icons.close, size: 12, color: Color(0xFF999999)), + ), + ), + ], ), ); } + Widget _buildEmptyState(String type) { + final idx = _tabTypes.indexOf(type); + final icon = _typeIcons[idx]; + final label = _typeLabels[idx]; + final hints = ['同步或手动添加影视类型', '同步或手动添加书籍类型', '同步或手动添加笔记标签']; + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, height: 64, + decoration: BoxDecoration(color: const Color(0xFFECECEC), borderRadius: BorderRadius.circular(18)), + child: Icon(icon, size: 28, color: const Color(0xFFCCCCCC)), + ), + const SizedBox(height: 16), + Text('暂无$label', + style: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB), fontWeight: FontWeight.w500)), + const SizedBox(height: 6), + Text(hints[idx], style: const TextStyle(fontSize: 12, color: Color(0xFFD5D5D5))), + ], + ), + ); + } + + // ─── 添加标签 ──────────────────────────────────────────────────────── + void _showAddDialog() { final controller = TextEditingController(); final type = _currentType; @@ -293,63 +327,33 @@ class _TagManagementPageState extends State { builder: (ctx) => AlertDialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: Text( - '添加${_typeLabels[_currentIndex]}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A)), - ), + title: Text('添加${_typeLabels[_currentIndex]}', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), content: TextField( - controller: controller, - autofocus: true, + controller: controller, autofocus: true, style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)), decoration: InputDecoration( hintText: '输入标签名称', - hintStyle: - const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)), - filled: true, - fillColor: const Color(0xFFFAFAFA), - contentPadding: - const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: - const BorderSide(color: Color(0xFF1A1A1A), width: 1), - ), + hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)), + filled: true, fillColor: const Color(0xFFFAFAFA), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)), ), - onSubmitted: (value) => - _doAddTag(ctx, controller.text.trim(), type), + onSubmitted: (value) => _doAddTag(ctx, controller.text.trim(), type), ), actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('取消', - style: TextStyle(color: Color(0xFF999999))), - ), + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))), Container( - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - borderRadius: BorderRadius.circular(20), - ), + decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)), child: Material( color: Colors.transparent, child: InkWell( - onTap: () => - _doAddTag(ctx, controller.text.trim(), type), + onTap: () => _doAddTag(ctx, controller.text.trim(), type), borderRadius: BorderRadius.circular(20), child: const Padding( - padding: - EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: Text('添加', - style: TextStyle( - fontSize: 14, - color: Colors.white, - fontWeight: FontWeight.w500)), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Text('添加', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)), ), ), ), @@ -359,15 +363,12 @@ class _TagManagementPageState extends State { ); } - Future _doAddTag( - BuildContext ctx, String name, String type) async { + Future _doAddTag(BuildContext ctx, String name, String type) async { if (name.isEmpty) return; try { await context.read().addTag(name, type); } catch (e) { - if (ctx.mounted) { - ToastUtil.show(ctx, '添加失败:该标签已存在'); - } + if (ctx.mounted) ToastUtil.show(ctx, '添加失败:该标签已存在'); return; } if (ctx.mounted) { @@ -377,6 +378,8 @@ class _TagManagementPageState extends State { await _loadTags(type); } + // ─── 重命名 ────────────────────────────────────────────────────────── + void _showRenameDialog(Map tag) { final controller = TextEditingController(text: tag['name'] as String); final tagId = tag['id'] as String; @@ -388,63 +391,32 @@ class _TagManagementPageState extends State { builder: (ctx) => AlertDialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: const Text( - '重命名标签', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A)), - ), + title: const Text('重命名标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), content: TextField( - controller: controller, - autofocus: true, + controller: controller, autofocus: true, style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)), decoration: InputDecoration( hintText: '输入新名称', - hintStyle: - const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)), - filled: true, - fillColor: const Color(0xFFFAFAFA), - contentPadding: - const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: - const BorderSide(color: Color(0xFF1A1A1A), width: 1), - ), + hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFAAAAAA)), + filled: true, fillColor: const Color(0xFFFAFAFA), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF1A1A1A), width: 1)), ), - onSubmitted: (value) => - _doRenameTag(ctx, tagId, value.trim(), type, oldName), + onSubmitted: (value) => _doRenameTag(ctx, tagId, value.trim(), type, oldName), ), actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('取消', - style: TextStyle(color: Color(0xFF999999))), - ), + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))), Container( - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - borderRadius: BorderRadius.circular(20), - ), + decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(20)), child: Material( color: Colors.transparent, child: InkWell( - onTap: () => - _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName), + onTap: () => _doRenameTag(ctx, tagId, controller.text.trim(), type, oldName), borderRadius: BorderRadius.circular(20), child: const Padding( - padding: - EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: Text('确定', - style: TextStyle( - fontSize: 14, - color: Colors.white, - fontWeight: FontWeight.w500)), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Text('确定', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)), ), ), ), @@ -454,22 +426,21 @@ class _TagManagementPageState extends State { ); } - Future _doRenameTag(BuildContext ctx, String tagId, String newName, - String type, String oldName) async { + Future _doRenameTag(BuildContext ctx, String tagId, String newName, String type, String oldName) async { if (newName.isEmpty || newName == oldName) { if (ctx.mounted) Navigator.pop(ctx); return; } - final success = - await context.read().renameTag(tagId, newName, type); + final success = await context.read().renameTag(tagId, newName, type); if (ctx.mounted) { Navigator.pop(ctx); - ToastUtil.show( - context, success ? '重命名成功' : '重命名失败:标签名已存在'); + ToastUtil.show(context, success ? '重命名成功' : '重命名失败:标签名已存在'); } if (success) await _loadTags(type); } + // ─── 删除(长按触发)────────────────────────────────────────────────── + void _showDeleteDialog(Map tag) { final tagId = tag['id'] as String; final type = tag['type'] as String; @@ -487,176 +458,92 @@ class _TagManagementPageState extends State { builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( backgroundColor: Colors.white, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Row( children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - name, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Color(0xFF666666)), - ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(12)), + child: Text(name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))), ), const SizedBox(width: 10), - const Text( - '删除标签', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A)), - ), + const Text('删除标签', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ], ), titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0), content: SizedBox( width: double.maxFinite, child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(ctx).size.height * 0.45, - ), + constraints: BoxConstraints(maxHeight: MediaQuery.of(ctx).size.height * 0.45), child: SingleChildScrollView( child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - const Text( - '删除后对已有条目的影响:', - style: TextStyle(fontSize: 13, color: Color(0xFF999999)), - ), - const SizedBox(height: 12), - _buildDeleteOption( - value: 'remove', - groupValue: selectedAction, - onChanged: (v) => setDialogState(() => selectedAction = v), - title: '从所有条目中移除该标签', - subtitle: '标签将从影视/书籍/笔记中清除', - ), - const SizedBox(height: 4), - _buildDeleteOption( - value: 'deleteOnly', - groupValue: selectedAction, - onChanged: (v) => setDialogState(() => selectedAction = v), - title: '仅删除标签', - subtitle: '保留已有条目上的标签名', - ), - const SizedBox(height: 4), - _buildDeleteOption( - value: 'replace', - groupValue: selectedAction, - onChanged: (v) => setDialogState(() => selectedAction = v), - title: '替换为其他标签', - ), - if (selectedAction == 'replace') - Padding( - padding: const EdgeInsets.only(left: 40, top: 10), - child: otherTags.isNotEmpty - ? Wrap( - spacing: 8, - runSpacing: 8, - children: otherTags.map((t) { - final isSelected = - replacementController.text == t; - return GestureDetector( - onTap: () { - replacementController.text = - isSelected ? '' : t; - setDialogState(() {}); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, vertical: 7), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF1A1A1A) - : const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? const Color(0xFF1A1A1A) - : const Color(0xFFE8E8E8), - width: 0.5, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + const Text('删除后对已有条目的影响:', style: TextStyle(fontSize: 13, color: Color(0xFF999999))), + const SizedBox(height: 12), + _buildDeleteOption(value: 'remove', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '从所有条目中移除该标签', subtitle: '标签将从影视/书籍/笔记中清除'), + const SizedBox(height: 4), + _buildDeleteOption(value: 'deleteOnly', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '仅删除标签', subtitle: '保留已有条目上的标签名'), + const SizedBox(height: 4), + _buildDeleteOption(value: 'replace', groupValue: selectedAction, onChanged: (v) => setDialogState(() => selectedAction = v), title: '替换为其他标签'), + if (selectedAction == 'replace') + Padding( + padding: const EdgeInsets.only(left: 40, top: 10), + child: otherTags.isNotEmpty + ? Wrap( + spacing: 8, runSpacing: 8, + children: otherTags.map((t) { + final isSelected = replacementController.text == t; + return GestureDetector( + onTap: () { + replacementController.text = isSelected ? '' : t; + setDialogState(() {}); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFFE8E8E8), width: 0.5), + ), + child: Text(t, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isSelected ? Colors.white : const Color(0xFF555555))), ), - ), - child: Text( - t, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: isSelected - ? Colors.white - : const Color(0xFF555555), - ), - ), - ), - ); - }).toList(), - ) - : Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(12), - ), - child: const Text('无其他标签可替换', - style: TextStyle( - fontSize: 13, - color: Color(0xFFAAAAAA))), - ), - ), - ], + ); + }).toList(), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(12)), + child: const Text('无其他标签可替换', style: TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))), + ), + ), + ], + ), ), ), ), - ), contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('取消', - style: TextStyle(color: Color(0xFF999999))), - ), + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消', style: TextStyle(color: Color(0xFF999999)))), Container( - decoration: BoxDecoration( - color: const Color(0xFFE53935), - borderRadius: BorderRadius.circular(20), - ), + decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(20)), child: Material( color: Colors.transparent, child: InkWell( onTap: () { String? replacement; if (selectedAction == 'replace') { - replacement = - replacementController.text.trim().isNotEmpty - ? replacementController.text.trim() - : null; + replacement = replacementController.text.trim().isNotEmpty ? replacementController.text.trim() : null; if (replacement == null) return; } - Navigator.pop(ctx, { - 'action': selectedAction, - 'replacement': replacement, - }); + Navigator.pop(ctx, {'action': selectedAction, 'replacement': replacement}); }, borderRadius: BorderRadius.circular(20), child: const Padding( - padding: - EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: Text('删除', - style: TextStyle( - fontSize: 14, - color: Colors.white, - fontWeight: FontWeight.w500)), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Text('删除', style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500)), ), ), ), @@ -669,19 +556,12 @@ class _TagManagementPageState extends State { if (result == null) return; final action = result['action'] as String; final replacement = result['replacement'] as String?; - if (action == 'deleteOnly') { - await context - .read() - .deleteTagOnly(tagId, type); + await context.read().deleteTagOnly(tagId, type); } else { - await context - .read() - .deleteTag(tagId, type, replacementName: replacement); - } - if (mounted) { - ToastUtil.show(context, '删除成功'); + await context.read().deleteTag(tagId, type, replacementName: replacement); } + if (mounted) ToastUtil.show(context, '删除成功'); await _loadTags(type); }); } @@ -701,54 +581,23 @@ class _TagManagementPageState extends State { decoration: BoxDecoration( color: selected ? const Color(0xFFFAFAFA) : Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all( - color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFEEEEEE), - width: selected ? 1 : 0.5, - ), + border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFEEEEEE), width: selected ? 1 : 0.5), ), child: Row( children: [ Container( - width: 18, - height: 18, + width: 18, height: 18, decoration: BoxDecoration( shape: BoxShape.circle, - border: Border.all( - color: selected - ? const Color(0xFF1A1A1A) - : const Color(0xFFCCCCCC), - width: selected ? 5 : 1.5, - ), + border: Border.all(color: selected ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), width: selected ? 5 : 1.5), ), ), const SizedBox(width: 12), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - title, - style: TextStyle( - fontSize: 14, - fontWeight: selected ? FontWeight.w500 : FontWeight.normal, - color: - selected ? const Color(0xFF1A1A1A) : const Color(0xFF666666), - ), - ), - if (subtitle != null) - Padding( - padding: const EdgeInsets.only(top: 2), - child: Text( - subtitle, - style: const TextStyle( - fontSize: 11, - color: Color(0xFFAAAAAA), - ), - ), - ), - ], - ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + Text(title, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w500 : FontWeight.normal, color: selected ? const Color(0xFF1A1A1A) : const Color(0xFF666666))), + if (subtitle != null) Padding(padding: const EdgeInsets.only(top: 2), child: Text(subtitle, style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA)))), + ]), ), ], ), diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index ac0d2cc..7e8fb0c 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -1,16 +1,15 @@ import 'dart:io'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../providers/app_provider.dart'; import '../utils/user_prefs.dart'; -import '../models/data_models.dart'; import '../pages/stroll_page.dart'; import '../pages/markdown_reader/md_reader_tab_page.dart'; import '../pages/tag_management_page.dart'; +import '../pages/profile_page.dart'; -/// 自定义左侧弹出菜单 - 极简主义设计 +/// 自定义侧边栏 class CustomDrawer extends StatefulWidget { const CustomDrawer({super.key}); @@ -29,55 +28,41 @@ class _CustomDrawerState extends State { Future _loadVersionInfo() async { final packageInfo = await PackageInfo.fromPlatform(); - setState(() { - _version = packageInfo.version; - }); + if (mounted) setState(() => _version = packageInfo.version); } @override Widget build(BuildContext context) { return Drawer( - backgroundColor: Colors.white, + backgroundColor: const Color(0xFFF8F8F8), child: SafeArea( child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), child: Column( - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 1. 顶部用户信息(头像左,名称/座右铭右) - _buildHeader(context), + // 头像 + 统计(独立卡片区域) + _buildProfileCard(context), - const Divider( - height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + const SizedBox(height: 16), - // 2. 统计数据 - _buildStatsSection(context), - - const Divider( - height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), - - // 3. 热力图 + // 热力图 _buildCalendarSection(context), - const Divider( - height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + const SizedBox(height: 16), - // 4. 回顾信息 - _buildMemorySection(context), + // 最近添加 + _buildRecentSection(context), - const Divider( - height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + const SizedBox(height: 16), - // 5. 功能模块:漫步 / Markdown 阅读 - _buildToolsSection(context), + // 功能入口 + _buildToolsCard(context), // 底部版本号 Padding( - padding: const EdgeInsets.all(20), - child: Text( - 'MookNote v$_version', - style: - const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB)), + padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), + child: Center( + child: Text('v$_version', style: const TextStyle(fontSize: 11, color: Color(0xFFD0D0D0))), ), ), ], @@ -87,98 +72,98 @@ class _CustomDrawerState extends State { ); } - /// 1. 构建头部 - 头像左侧,名称/座右铭右侧 - Widget _buildHeader(BuildContext context) { + // ─── 头像 + 统计卡片 ───────────────────────────────────────────────── + + Widget _buildProfileCard(BuildContext context) { return Consumer( builder: (context, provider, child) { final userPrefs = UserPrefs(); final nickname = userPrefs.nickname; final motto = userPrefs.motto; final avatarPath = userPrefs.avatarPath; - - return Padding( - padding: const EdgeInsets.fromLTRB(24, 40, 24, 24), - child: Row( - children: [ - // 左侧:头像 - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFF5F5F5), - border: - Border.all(color: const Color(0xFFE8E8E8), width: 0.5), - ), - clipBehavior: Clip.antiAlias, - child: avatarPath != null && avatarPath.isNotEmpty - ? Image.file( - File(avatarPath), - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildAvatarPlaceholder(), - ) - : _buildAvatarPlaceholder(), - ), - - const SizedBox(width: 16), - - // 右侧:名称 + 座右铭 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - nickname, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 4), - Text( - motto, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildAvatarPlaceholder() { - return const Center( - child: Icon(Icons.person_outline, size: 28, color: Color(0xFFAAAAAA)), - ); - } - - /// 2. 统计数据:观影xxx 阅读xxx 笔记xxx - Widget _buildStatsSection(BuildContext context) { - return Consumer( - builder: (context, provider, child) { final movieCount = provider.movies.where((m) => !m.isDeleted).length; final bookCount = provider.books.length; final noteCount = provider.notes.length; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), - child: Row( + return Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStatItem('观影', movieCount), - const SizedBox(width: 32), - _buildStatItem('阅读', bookCount), - const SizedBox(width: 32), - _buildStatItem('笔记', noteCount), + // 头像 + 名称/座右铭 + 统计 + Row( + children: [ + // 头像 + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5), + ), + clipBehavior: Clip.antiAlias, + child: avatarPath != null && avatarPath.isNotEmpty + ? Image.file(File(avatarPath), fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB))) + : const Icon(Icons.person_outline, size: 26, color: Color(0xFFBBBBBB)), + ), + const SizedBox(width: 14), + // 名称 + 座右铭 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(nickname, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), + const SizedBox(height: 2), + Text(motto, maxLines: 1, overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))), + ], + ), + ), + ], + ), + + const SizedBox(height: 16), + const Divider(height: 1, color: Color(0xFFF0F0F0)), + const SizedBox(height: 14), + + // 统计数字 + _buildProfileStatRow(Icons.movie_outlined, movieCount, '观影'), + const SizedBox(height: 12), + _buildProfileStatRow(Icons.menu_book_outlined, bookCount, '阅读'), + const SizedBox(height: 12), + _buildProfileStatRow(Icons.note_outlined, noteCount, '笔记'), + + // 设置入口 + const SizedBox(height: 14), + const Divider(height: 1, color: Color(0xFFF0F0F0)), + InkWell( + onTap: () { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsPage())); + }, + borderRadius: BorderRadius.circular(8), + child: const Padding( + padding: EdgeInsets.only(top: 14), + child: Row( + children: [ + Icon(Icons.settings_outlined, size: 16, color: Color(0xFF999999)), + SizedBox(width: 8), + Text('设置', style: TextStyle(fontSize: 13, color: Color(0xFF888888))), + Spacer(), + Icon(Icons.chevron_right, size: 14, color: Color(0xFFD0D0D0)), + ], + ), + ), + ), ], ), ); @@ -186,202 +171,105 @@ class _CustomDrawerState extends State { ); } - Widget _buildStatItem(String label, int count) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _buildProfileStatRow(IconData icon, int count, String label) { + return Row( children: [ - Text( - '$count', - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w700, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 2), - Text( - label, - style: const TextStyle(fontSize: 12, color: Color(0xFF999999)), - ), + Icon(icon, size: 16, color: const Color(0xFF888888)), + const SizedBox(width: 10), + Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), + const Spacer(), + Text(_formatCount(count), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), ], ); } - /// 5. 功能模块:漫步 / Markdown 阅读 - Widget _buildToolsSection(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFFAFAFA), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFEEEEEE), width: 0.5), - ), - child: Column( - mainAxisSize: MainAxisSize.min, + String _formatCount(int count) { + if (count >= 10000) return '${(count / 10000).toStringAsFixed(1)}万'; + if (count >= 1000) return '${(count / 1000).toStringAsFixed(1)}k'; + return count.toString(); + } + + // ─── 功能入口卡片 ──────────────────────────────────────────────────── + + Widget _buildToolsCard(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + _buildToolItem(Icons.explore_outlined, '漫步', () { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute(builder: (_) => const StrollPage())); + }, topRounded: true), + const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)), + _buildToolItem(Icons.label_outline, '标签管理', () { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute(builder: (_) => const TagManagementPage())); + }), + const Divider(height: 1, indent: 52, endIndent: 20, color: Color(0xFFF0F0F0)), + _buildToolItem(Icons.description_outlined, 'MD阅读', () { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute(builder: (_) => const MdReaderTabPage())); + }, bottomRounded: true), + ], + ), + ); + } + + Widget _buildToolItem(IconData icon, String title, VoidCallback onTap, {bool topRounded = false, bool bottomRounded = false}) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.only( + topLeft: topRounded ? const Radius.circular(16) : Radius.zero, + topRight: topRounded ? const Radius.circular(16) : Radius.zero, + bottomLeft: bottomRounded ? const Radius.circular(16) : Radius.zero, + bottomRight: bottomRounded ? const Radius.circular(16) : Radius.zero, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), + child: Row( children: [ - // 漫步 - InkWell( - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const StrollPage()), - ); - }, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(10)), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 13, horizontal: 16), - child: Row( - children: [ - Icon(Icons.explore_outlined, - size: 18, color: Color(0xFF666666)), - SizedBox(width: 10), - Expanded( - child: Text( - '漫步', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A)), - ), - ), - Text( - '随机发现一条内容', - style: TextStyle(fontSize: 11, color: Color(0xFFBBBBBB)), - ), - SizedBox(width: 6), - Icon(Icons.chevron_right, - size: 16, color: Color(0xFFCCCCCC)), - ], - ), - ), - ), - - // 分隔线 - const Divider( - height: 0.5, thickness: 0.5, color: Color(0xFFEEEEEE)), - - // 标签管理 - InkWell( - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const TagManagementPage()), - ); - }, - borderRadius: BorderRadius.zero, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 13, horizontal: 16), - child: Row( - children: [ - Icon(Icons.label_outline, - size: 18, color: Color(0xFF666666)), - SizedBox(width: 10), - Expanded( - child: Text( - '标签管理', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A)), - ), - ), - Text( - '管理标签', - style: TextStyle(fontSize: 11, color: Color(0xFFBBBBBB)), - ), - SizedBox(width: 6), - Icon(Icons.chevron_right, - size: 16, color: Color(0xFFCCCCCC)), - ], - ), - ), - ), - - // 分隔线 - const Divider( - height: 0.5, thickness: 0.5, color: Color(0xFFEEEEEE)), - - // Markdown 阅读 - InkWell( - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const MdReaderTabPage()), - ); - }, - borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(10)), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 13, horizontal: 16), - child: Row( - children: [ - Icon(Icons.description_outlined, - size: 18, color: Color(0xFF666666)), - SizedBox(width: 10), - Expanded( - child: Text( - 'MD阅读', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF1A1A1A)), - ), - ), - Text( - '浏览本地 md 文件', - style: TextStyle(fontSize: 11, color: Color(0xFFBBBBBB)), - ), - SizedBox(width: 6), - Icon(Icons.chevron_right, - size: 16, color: Color(0xFFCCCCCC)), - ], - ), - ), - ), + Icon(icon, size: 20, color: const Color(0xFF555555)), + const SizedBox(width: 12), + Expanded(child: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))), + const Icon(Icons.chevron_right, size: 16, color: Color(0xFFD0D0D0)), ], ), ), ); } + // ─── 热力图(保持现状) ────────────────────────────────────────────── + Widget _buildCalendarSection(BuildContext context) { return Consumer( builder: (context, provider, child) { final Map dailyCounts = {}; - for (final movie in provider.movies.where((m) => !m.isDeleted)) { - final date = DateTime( - movie.createdAt.year, movie.createdAt.month, movie.createdAt.day); + final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day); dailyCounts[date] = (dailyCounts[date] ?? 0) + 1; } for (final book in provider.books.where((b) => !b.isDeleted)) { - final date = DateTime( - book.createdAt.year, book.createdAt.month, book.createdAt.day); + final date = DateTime(book.createdAt.year, book.createdAt.month, book.createdAt.day); dailyCounts[date] = (dailyCounts[date] ?? 0) + 1; } for (final note in provider.notes.where((n) => !n.isDeleted)) { - final date = DateTime( - note.createdAt.year, note.createdAt.month, note.createdAt.day); + final date = DateTime(note.createdAt.year, note.createdAt.month, note.createdAt.day); dailyCounts[date] = (dailyCounts[date] ?? 0) + 1; } - // 计算最大计数用于颜色映射 int maxCount = 0; for (final c in dailyCounts.values) { if (c > maxCount) maxCount = c; } if (maxCount == 0) maxCount = 1; - // 从上周日开始,往前推 N 周 final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); - final daysSinceSunday = today.weekday % 7; // Sunday=0 + final daysSinceSunday = today.weekday % 7; final lastSunday = today.subtract(Duration(days: daysSinceSunday)); const totalWeeks = 20; @@ -389,119 +277,77 @@ class _CustomDrawerState extends State { const cellSize = 13.0; const cellGap = 3.0; - // 构建网格:行=星期几(0=日..6=六),列=周(0=最旧..19=最新) - final cells = - List.generate(weekDays, (_) => List.generate(totalWeeks, (_) => 0)); - + final cells = List.generate(weekDays, (_) => List.generate(totalWeeks, (_) => 0)); for (int week = 0; week < totalWeeks; week++) { for (int day = 0; day < weekDays; day++) { - final date = lastSunday.subtract( - Duration(days: (totalWeeks - 1 - week) * 7 + (6 - day))); + final date = lastSunday.subtract(Duration(days: (totalWeeks - 1 - week) * 7 + (6 - day))); cells[day][week] = dailyCounts[date] ?? 0; } } - // 月份标签:找出每个月第一天所在的列 final monthLabels = {}; for (int week = 0; week < totalWeeks; week++) { - final date = - lastSunday.subtract(Duration(days: (totalWeeks - 1 - week) * 7)); + final date = lastSunday.subtract(Duration(days: (totalWeeks - 1 - week) * 7)); final key = date.month; if (!monthLabels.containsKey(key) || date.day <= 7) { monthLabels[week] = '${date.month}月'; } } - - // 只保留首、中间、尾三个月份标签 final sortedWeeks = monthLabels.keys.toList()..sort(); - final keepWeeks = { - sortedWeeks.first, - sortedWeeks[sortedWeeks.length ~/ 2], - sortedWeeks.last, - }; + final keepWeeks = {sortedWeeks.first, sortedWeeks[sortedWeeks.length ~/ 2], sortedWeeks.last}; return Container( - width: double.infinity, - padding: const EdgeInsets.all(20), + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(18), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ - Icon(Icons.calendar_today, - size: 14, color: Color(0xFF999999)), + Icon(Icons.calendar_today, size: 14, color: Color(0xFF999999)), SizedBox(width: 8), - Text( - '热力图', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Color(0xFF666666)), - ), + Text('热力图', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))), ], ), - const SizedBox(height: 16), - - // 热力图主体 + const SizedBox(height: 14), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 月份标签行 SizedBox( height: 16, child: Row( children: List.generate(totalWeeks, (week) { - final label = keepWeeks.contains(week) - ? monthLabels[week] - : null; + final label = keepWeeks.contains(week) ? monthLabels[week] : null; return SizedBox( width: cellSize + cellGap, - child: label != null - ? Text(label, - style: const TextStyle( - fontSize: 9, color: Color(0xFFBBBBBB))) - : null, + child: label != null ? Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))) : null, ); }), ), ), const SizedBox(height: 2), - - // 日期网格行 - ...List.generate(weekDays, (day) { - return Row( - children: List.generate(totalWeeks, (week) { - final count = cells[day][week]; - final color = _heatmapColor(count, maxCount); - return Container( - width: cellSize, - height: cellSize, - margin: EdgeInsets.only( - right: week < totalWeeks - 1 ? cellGap : 0, - bottom: day < weekDays - 1 ? cellGap : 0, - ), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(2), - ), - ); - }), - ); - }), + ...List.generate(weekDays, (day) => Row( + children: List.generate(totalWeeks, (week) { + final count = cells[day][week]; + return Container( + width: cellSize, height: cellSize, + margin: EdgeInsets.only(right: week < totalWeeks - 1 ? cellGap : 0, bottom: day < weekDays - 1 ? cellGap : 0), + decoration: BoxDecoration(color: _heatmapColor(count, maxCount), borderRadius: BorderRadius.circular(2)), + ); + }), + )), ], ), ), - - // 图例 - const SizedBox(height: 10), + const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - const Text('少', - style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))), + const Text('少', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))), const SizedBox(width: 3), _legendCell(const Color(0xFFF0F0F0)), _legendCell(const Color(0xFFC8E6C9)), @@ -509,8 +355,7 @@ class _CustomDrawerState extends State { _legendCell(const Color(0xFF2E7D32)), _legendCell(const Color(0xFF1B5E20)), const SizedBox(width: 3), - const Text('多', - style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))), + const Text('多', style: TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))), ], ), ], @@ -530,126 +375,51 @@ class _CustomDrawerState extends State { } Widget _legendCell(Color color) { - return Container( - width: 10, - height: 10, - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: - BoxDecoration(color: color, borderRadius: BorderRadius.circular(2)), - ); + return Container(width: 10, height: 10, margin: const EdgeInsets.symmetric(horizontal: 1), + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(2))); } - /// 4. 回顾信息 - Widget _buildMemorySection(BuildContext context) { + // ─── 最近添加 ──────────────────────────────────────────────────────── + + Widget _buildRecentSection(BuildContext context) { return Consumer( builder: (context, provider, child) { - final memoryItem = _getRandomMemoryItem(provider); - - if (memoryItem == null) { - return const SizedBox.shrink(); - } - - final memoryText = _buildMemoryText(memoryItem); - final timeAgo = _getTimeAgoText(memoryItem.date); + final recent = _getRecentItems(provider); + if (recent.isEmpty) return const SizedBox.shrink(); return Container( - width: double.infinity, - padding: const EdgeInsets.all(20), + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(18), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 标题 - Row( + const Row( children: [ - const Icon(Icons.history, size: 14, color: Color(0xFF999999)), - const SizedBox(width: 8), - const Text( - '回顾', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Color(0xFF666666)), - ), + Icon(Icons.schedule, size: 14, color: Color(0xFF999999)), + SizedBox(width: 8), + Text('最近添加', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))), ], ), - const SizedBox(height: 12), - - // 内容卡片 - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 头图 - if (memoryItem.imagePath != null && - memoryItem.imagePath!.isNotEmpty) - Container( - width: 56, - height: 72, - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(4), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.file( - File(memoryItem.imagePath!), - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - const Icon(Icons.image, color: Color(0xFFCCCCCC)), - ), - ), - ) - else if (memoryItem.type != 'note') - Container( - width: 56, - height: 72, - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(4), - ), - child: Icon( - memoryItem.type == 'movie' - ? Icons.movie - : Icons.menu_book, - color: const Color(0xFFCCCCCC), - size: 22, - ), + const SizedBox(height: 14), + ...recent.take(4).map((item) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Icon( + item.type == 'movie' ? Icons.movie_outlined : item.type == 'book' ? Icons.menu_book_outlined : Icons.note_outlined, + size: 14, color: const Color(0xFFBBBBBB), ), - - // 文字 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - timeAgo, - style: const TextStyle( - fontSize: 10, color: Color(0xFF999999)), - ), - ), - const SizedBox(height: 8), - Text( - memoryText, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF1A1A1A), - height: 1.5), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ], + const SizedBox(width: 10), + Expanded( + child: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13, color: Color(0xFF444444))), ), - ), - ], - ), + const SizedBox(width: 8), + Text(_recentTimeAgo(item.date), style: const TextStyle(fontSize: 10, color: Color(0xFFCCCCCC))), + ], + ), + )), ], ), ); @@ -657,85 +427,34 @@ class _CustomDrawerState extends State { ); } - _MemoryItem? _getRandomMemoryItem(AppProvider provider) { - final now = DateTime.now(); - final candidates = <_MemoryItem>[]; - - for (final movie in provider.movies.where((m) => !m.isDeleted)) { - candidates.add(_MemoryItem( - type: 'movie', - title: movie.title, - date: movie.createdAt, - imagePath: movie.posterPath, - )); + List<_RecentItem> _getRecentItems(AppProvider provider) { + final items = <_RecentItem>[]; + for (final m in provider.movies.where((m) => !m.isDeleted)) { + items.add(_RecentItem(type: 'movie', title: m.title, date: m.createdAt)); } - - for (final book in provider.books.where((b) => !b.isDeleted)) { - candidates.add(_MemoryItem( - type: 'book', - title: book.title, - date: book.createdAt, - imagePath: book.coverPath, - )); + for (final b in provider.books.where((b) => !b.isDeleted)) { + items.add(_RecentItem(type: 'book', title: b.title, date: b.createdAt)); } - - if (candidates.isEmpty) return null; - - final oneMonthAgo = now.subtract(const Duration(days: 30)); - final threeMonthsAgo = now.subtract(const Duration(days: 90)); - final sixMonthsAgo = now.subtract(const Duration(days: 180)); - final oneYearAgo = now.subtract(const Duration(days: 365)); - - final memoryCandidates = candidates.where((item) { - return _isInTimeRange( - item.date, oneMonthAgo, threeMonthsAgo, sixMonthsAgo, oneYearAgo); - }).toList(); - - final random = Random(); - final selectedList = - memoryCandidates.isNotEmpty ? memoryCandidates : candidates; - return selectedList[random.nextInt(selectedList.length)]; + for (final n in provider.notes.where((n) => !n.isDeleted)) { + items.add(_RecentItem(type: 'note', title: n.title.isNotEmpty ? n.title : '随手记', date: n.createdAt)); + } + items.sort((a, b) => b.date.compareTo(a.date)); + return items; } - bool _isInTimeRange(DateTime date, DateTime oneMonth, DateTime threeMonths, - DateTime sixMonths, DateTime oneYear) { - if (_isCloseTo(date, oneMonth)) return true; - if (_isCloseTo(date, threeMonths, days: 14)) return true; - if (_isCloseTo(date, sixMonths, days: 30)) return true; - if (_isCloseTo(date, oneYear, days: 30)) return true; - return false; - } - - bool _isCloseTo(DateTime date, DateTime target, {int days = 7}) { - final diff = date.difference(target).inDays.abs(); - return diff <= days; - } - - String _buildMemoryText(_MemoryItem item) { - return '《${item.title}》'; - } - - String _getTimeAgoText(DateTime date) { - final now = DateTime.now(); - final diff = now.difference(date); - if (diff.inDays >= 365) return '1年前'; - if (diff.inDays >= 180) return '6个月前'; - if (diff.inDays >= 90) return '3个月前'; - if (diff.inDays >= 30) return '1个月前'; - return '${diff.inDays}天前'; + String _recentTimeAgo(DateTime date) { + final diff = DateTime.now().difference(date); + if (diff.inDays >= 365) return '${(diff.inDays / 365).floor()}年前'; + if (diff.inDays >= 30) return '${(diff.inDays / 30).floor()}月前'; + if (diff.inDays > 0) return '${diff.inDays}天前'; + if (diff.inHours > 0) return '${diff.inHours}小时前'; + return '刚刚'; } } -class _MemoryItem { +class _RecentItem { final String type; final String title; final DateTime date; - final String? imagePath; - - _MemoryItem({ - required this.type, - required this.title, - required this.date, - this.imagePath, - }); + _RecentItem({required this.type, required this.title, required this.date}); }