diff --git a/lib/main.dart b/lib/main.dart index 67a488f..0301ee2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -141,12 +141,9 @@ class _MyAppState extends State with WidgetsBindingObserver { context: context, builder: (ctx) => AlertDialog( backgroundColor: colors.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: Row(children: [ - Icon(Icons.system_update_outlined, color: colors.primary, size: 24), - const SizedBox(width: 10), - Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), - ]), + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), content: Text('新版本 $version 已发布,是否下载更新?', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), actions: [ @@ -155,10 +152,9 @@ class _MyAppState extends State with WidgetsBindingObserver { UserPrefs().setDismissedVersion(version); Navigator.pop(ctx); }, - child: Text('不再显示', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.5))), + child: Text('不再显示', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))), ), - const SizedBox(width: 8), - FilledButton.icon( + ElevatedButton( onPressed: () async { Navigator.pop(ctx); try { @@ -171,11 +167,15 @@ class _MyAppState extends State with WidgetsBindingObserver { } } }, - icon: const Icon(Icons.open_in_browser, size: 18), - label: const Text('去官网下载'), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('去官网下载'), ), ], - actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ); } diff --git a/lib/pages/book/book_tab_page.dart b/lib/pages/book/book_tab_page.dart index 72c2be3..ab29aa6 100644 --- a/lib/pages/book/book_tab_page.dart +++ b/lib/pages/book/book_tab_page.dart @@ -232,15 +232,6 @@ class _BookTabPageState extends State { child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), const SizedBox(height: 20), Text('暂无$statusText的书籍', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), - const SizedBox(height: 24), - InkWell(onTap: () { - final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'}; - Navigator.pushNamed(context, '/book-form', arguments: {'initialStatus': statusMap[statusIndex]!}); - }, - child: Container(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)), - child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary))), - ), ])); } } diff --git a/lib/pages/changelog_page.dart b/lib/pages/changelog_page.dart index adffdf1..13d5ba7 100644 --- a/lib/pages/changelog_page.dart +++ b/lib/pages/changelog_page.dart @@ -47,27 +47,43 @@ class _ChangelogPageState extends State { localVersion: localVersion, ); } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('已是最新版本(当前 $localVersion)'), - duration: const Duration(seconds: 2), - )); + _showNoUpdateDialog(localVersion); } } catch (_) {} if (mounted) setState(() => _checking = false); } + void _showNoUpdateDialog(String localVersion) { + final colors = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: colors.surface, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('检查更新', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: Text('已是最新版本(当前 $localVersion)', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('好的', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))), + ), + ], + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ); + } + void _showUpdateDialog({required String version, String? localVersion}) { final colors = Theme.of(context).colorScheme; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: colors.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: Row(children: [ - Icon(Icons.system_update_outlined, color: colors.primary, size: 24), - const SizedBox(width: 10), - Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), - ]), + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -84,21 +100,24 @@ class _ChangelogPageState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(ctx), - child: Text('稍后再说', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.5))), + child: Text('稍后再说', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))), ), - const SizedBox(width: 8), - FilledButton.icon( + ElevatedButton( onPressed: () async { Navigator.pop(ctx); try { await launchUrl(Uri.parse(_websiteUrl), mode: LaunchMode.externalApplication); } catch (_) {} }, - icon: const Icon(Icons.open_in_browser, size: 18), - label: const Text('去官网下载'), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('去官网下载'), ), ], - actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ); } diff --git a/lib/pages/movies/movie_tab_page.dart b/lib/pages/movies/movie_tab_page.dart index 47529d4..d1364a1 100644 --- a/lib/pages/movies/movie_tab_page.dart +++ b/lib/pages/movies/movie_tab_page.dart @@ -309,18 +309,6 @@ class _MovieTabPageState extends State { child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), const SizedBox(height: 20), Text('暂无$statusText的影片', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), - const SizedBox(height: 24), - InkWell( - onTap: () { - final statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'}; - Navigator.pushNamed(context, '/movie-form', arguments: {'initialStatus': statusMap[statusIndex]!}); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)), - child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)), - ), - ), ])); } } diff --git a/lib/pages/note/note_tab_page.dart b/lib/pages/note/note_tab_page.dart index c9dd036..9449805 100644 --- a/lib/pages/note/note_tab_page.dart +++ b/lib/pages/note/note_tab_page.dart @@ -322,12 +322,6 @@ class _NoteTabPageState extends State { child: Icon(Icons.note_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), const SizedBox(height: 20), Text('暂无笔记', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), - const SizedBox(height: 24), - InkWell(onTap: () => Navigator.pushNamed(context, '/note-form'), - child: Container(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)), - child: Text('添加记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary))), - ), ])); } } diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index c836bc1..38cb010 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -136,7 +136,7 @@ class _ProfilePageState extends State { : Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)), ), ), - const SizedBox(width: 16), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -181,7 +181,7 @@ class _ProfilePageState extends State { children: [ Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.3)), const SizedBox(width: 8), - Text(label, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + Text(label, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), const Spacer(), Text(count, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), ], @@ -243,7 +243,7 @@ class _ProfilePageState extends State { final colors = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), - child: Text(title, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.3))), + child: Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.3))), ); } @@ -385,18 +385,18 @@ class _ProfilePageState extends State { const SizedBox(height: 16), ListTile( contentPadding: EdgeInsets.zero, - leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.folder_outlined, color: colors.onSurface.withValues(alpha: 0.6))), - title: Text('本地备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), - subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.folder_outlined, color: colors.onSurface.withValues(alpha: 0.6))), + title: Text('本地备份', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)), onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); }, ), Divider(height: 0.5, color: colors.outlineVariant), ListTile( contentPadding: EdgeInsets.zero, - leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.cloud_outlined, color: colors.onSurface.withValues(alpha: 0.6))), - title: Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), - subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.cloud_outlined, color: colors.onSurface.withValues(alpha: 0.6))), + title: Text('云备份', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)), onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); }, ), @@ -450,22 +450,7 @@ class _SettingsPageState extends State { appBar: AppBar(title: const Text('设置')), body: ListView( children: [ - _buildSectionHeader('数据管理'), - _buildActionItem( - icon: Icons.cleaning_services_outlined, - title: '清除缓存数据', - subtitle: '清理未在数据库中引用的图片文件', - onTap: () => _showClearCacheDialog(context), - ), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem( - icon: Icons.swipe_vertical_outlined, - title: '底部导航栏滚动隐藏', - subtitle: '下滑时自动隐藏底部导航栏', - value: _hideBottomNavOnScroll, - onChanged: _toggleHideBottomNavOnScroll, - ), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), + _buildSectionHeader('显示设置'), _buildNavigationItem( icon: Icons.apps_outlined, title: '应用图标', @@ -483,14 +468,37 @@ class _SettingsPageState extends State { _buildNavigationItem( icon: Icons.dashboard_outlined, title: '布局设置', - subtitle: '笔记、影视、阅读的展示样式', + subtitle: '影视、阅读、笔记的展示样式', onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())), ), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSectionHeader('外观'), _buildThemeModeSelector(), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), + _buildSectionHeader('其他设置'), + _buildSwitchItem( + icon: Icons.swipe_vertical_outlined, + title: '底部导航栏滚动隐藏', + subtitle: '下滑时自动隐藏底部导航栏', + value: _hideBottomNavOnScroll, + onChanged: _toggleHideBottomNavOnScroll, + ), + Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), + _buildSectionHeader('数据管理'), + _buildActionItem( + icon: Icons.cleaning_services_outlined, + title: '清除缓存数据', + subtitle: '清理未在数据库中引用的图片文件', + onTap: () => _showClearCacheDialog(context), + ), + Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), _buildSectionHeader('帮助'), + _buildActionItem( + icon: Icons.update_outlined, + title: '更新日志', + subtitle: '查看版本更新内容', + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ChangelogPage())), + ), + Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), _buildLinkItem( context: context, icon: Icons.help_outline, @@ -499,13 +507,6 @@ class _SettingsPageState extends State { url: 'https://mooknote.iletter.top/#/guide', ), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildActionItem( - icon: Icons.update_outlined, - title: '更新日志', - subtitle: '查看版本更新内容', - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ChangelogPage())), - ), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), ], ), ); @@ -524,16 +525,16 @@ class _SettingsPageState extends State { return InkWell( onTap: () => _showThemeModePicker(), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ - Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[_themeMode], color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - const SizedBox(width: 16), + Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[_themeMode], color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('主题模式', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), + Text('主题模式', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), const SizedBox(height: 2), - Text(_themeModeLabels[_themeMode], style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + Text(_themeModeLabels[_themeMode], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), ]), ), Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20), @@ -560,8 +561,8 @@ class _SettingsPageState extends State { const SizedBox(height: 16), for (int i = 0; i < _themeModeLabels.length; i++) ListTile( - leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[i], color: colors.onSurface.withValues(alpha: 0.6))), - title: Text(_themeModeLabels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), + leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(_themeModeIcons[i], color: colors.onSurface.withValues(alpha: 0.6))), + title: Text(_themeModeLabels[i], style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), trailing: _themeMode == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null, onTap: () async { await _setThemeMode(i); Navigator.pop(ctx); }, ), @@ -590,16 +591,16 @@ class _SettingsPageState extends State { return InkWell( onTap: () => onChanged(!value), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ - Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - const SizedBox(width: 16), + Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), + Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), const SizedBox(height: 2), - Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), ]), ), Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline), @@ -612,8 +613,8 @@ class _SettingsPageState extends State { Widget _buildSectionHeader(String title) { final colors = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.fromLTRB(24, 32, 24, 12), - child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), + padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), + child: Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface)), ); } @@ -622,16 +623,16 @@ class _SettingsPageState extends State { return InkWell( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ - Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - const SizedBox(width: 16), + Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), + Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), const SizedBox(height: 2), - Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), ]), ), Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20), @@ -646,16 +647,16 @@ class _SettingsPageState extends State { return InkWell( onTap: () => _launchUrl(context, url), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ - Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - const SizedBox(width: 16), + Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), + Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), const SizedBox(height: 2), - Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), ]), ), Icon(Icons.open_in_new, color: colors.onSurface.withValues(alpha: 0.25), size: 18), @@ -670,16 +671,16 @@ class _SettingsPageState extends State { return InkWell( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ - Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - const SizedBox(width: 16), + Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), + Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), const SizedBox(height: 2), - Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), ]), ), ], @@ -694,16 +695,24 @@ class _SettingsPageState extends State { context: pageContext, builder: (dialogContext) => AlertDialog( backgroundColor: colors.surface, elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - title: const Text('清除缓存数据'), - content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('清除缓存数据', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), actions: [ TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), - TextButton(onPressed: () async { - Navigator.pop(dialogContext); - await _clearCacheData(pageContext); - }, child: Text('确定', style: TextStyle(color: colors.error))), + ElevatedButton( + onPressed: () async { + Navigator.pop(dialogContext); + await _clearCacheData(pageContext); + }, + style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), + child: const Text('清除'), + ), ], + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ); } @@ -740,7 +749,7 @@ class _SettingsPageState extends State { final imagesDir = Directory('${appDir.path}/images'); if (!await imagesDir.exists()) return 0; await for (final entity in imagesDir.list(recursive: true, followLinks: false)) { - if (entity is File && !dbImagePaths.contains(entity.path)) { + if (entity is File && !dbImagePaths.contains(entity.path) && !path.basename(entity.path).startsWith('avatar')) { try { await entity.delete(); deletedCount++; } catch (_) {} } } @@ -830,7 +839,7 @@ class _MainContentSettingsPageState extends State { Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))), + child: Text('至少保留一个模块,关闭后对应标签页将不再显示。', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))), ), ], ), @@ -840,8 +849,8 @@ class _MainContentSettingsPageState extends State { Widget _buildSectionHeader(String title) { final colors = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), - child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), + padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), + child: Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface)), ); } @@ -849,9 +858,9 @@ class _MainContentSettingsPageState extends State { final colors = Theme.of(context).colorScheme; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), - subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + title: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + subtitle: Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), trailing: Switch(value: value, onChanged: onChanged, activeColor: colors.primary, activeTrackColor: colors.primary.withOpacity(0.3), inactiveThumbColor: colors.surface, inactiveTrackColor: colors.outline), ); } @@ -863,9 +872,9 @@ class _MainContentSettingsPageState extends State { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.home_outlined, color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - title: Text('默认启动标签', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), - subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(Icons.home_outlined, color: colors.onSurface.withValues(alpha: 0.6), size: 18)), + title: Text('默认启动标签', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -895,8 +904,8 @@ class _MainContentSettingsPageState extends State { const SizedBox(height: 16), for (int i = 0; i < labels.length; i++) ListTile( - leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: colors.onSurface.withValues(alpha: 0.6))), - title: Text(labels[i], style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), + leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icons[i], color: colors.onSurface.withValues(alpha: 0.6))), + title: Text(labels[i], style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), trailing: _defaultTabIndex == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null, onTap: () async { await _userPrefs.setDefaultMainTabIndex(i); setState(() => _defaultTabIndex = i); Navigator.pop(ctx); }, ), @@ -938,24 +947,21 @@ class _LayoutSettingsPageState extends State { backgroundColor: colors.surface, appBar: AppBar(title: const Text('布局设置')), body: ListView( + padding: const EdgeInsets.symmetric(vertical: 8), children: [ - _buildSectionHeader('笔记布局'), - _buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列列表,简洁清晰', value: 0, groupValue: _noteLayout, onTap: () => _setLayout('note', 0)), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildLayoutOption(icon: Icons.grid_view_outlined, title: '瀑布流布局', subtitle: '双列卡片,图文并茂', value: 1, groupValue: _noteLayout, onTap: () => _setLayout('note', 1)), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildLayoutOption(icon: Icons.timeline_outlined, title: '时间线布局', subtitle: '按时间排列,纵览全局', value: 2, groupValue: _noteLayout, onTap: () => _setLayout('note', 2)), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSectionHeader('影视布局'), - _buildLayoutOption(icon: Icons.grid_view_outlined, title: '海报网格', subtitle: '三列海报,赏心悦目', value: 0, groupValue: _movieLayout, onTap: () => _setLayout('movie', 0)), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _movieLayout, onTap: () => _setLayout('movie', 1)), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSectionHeader('阅读布局'), - _buildLayoutOption(icon: Icons.grid_view_outlined, title: '封面网格', subtitle: '三列封面,清新雅致', value: 0, groupValue: _bookLayout, onTap: () => _setLayout('book', 0)), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _bookLayout, onTap: () => _setLayout('book', 1)), - Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), + _buildSection('影视布局', [ + ButtonSegment(value: 0, icon: Icon(Icons.grid_view_outlined, size: 16), label: Text('海报网格', style: TextStyle(fontSize: 12))), + ButtonSegment(value: 1, icon: Icon(Icons.view_list_outlined, size: 16), label: Text('列表', style: TextStyle(fontSize: 12))), + ], _movieLayout, (v) => _setLayout('movie', v)), + _buildSection('阅读布局', [ + ButtonSegment(value: 0, icon: Icon(Icons.grid_view_outlined, size: 16), label: Text('封面网格', style: TextStyle(fontSize: 12))), + ButtonSegment(value: 1, icon: Icon(Icons.view_list_outlined, size: 16), label: Text('列表', style: TextStyle(fontSize: 12))), + ], _bookLayout, (v) => _setLayout('book', v)), + _buildSection('笔记布局', [ + ButtonSegment(value: 0, icon: Icon(Icons.view_list_outlined, size: 16), label: Text('列表', style: TextStyle(fontSize: 12))), + ButtonSegment(value: 1, icon: Icon(Icons.grid_view_outlined, size: 16), label: Text('瀑布流', style: TextStyle(fontSize: 12))), + ButtonSegment(value: 2, icon: Icon(Icons.timeline_outlined, size: 16), label: Text('时间线', style: TextStyle(fontSize: 12))), + ], _noteLayout, (v) => _setLayout('note', v)), ], ), ); @@ -969,24 +975,34 @@ class _LayoutSettingsPageState extends State { } } - Widget _buildSectionHeader(String title) { + Widget _buildSection(String title, List> segments, int selected, ValueChanged onChanged) { final colors = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), - child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), - ); - } - - Widget _buildLayoutOption({required IconData icon, required String title, required String subtitle, required int value, required int groupValue, required VoidCallback onTap}) { - final colors = Theme.of(context).colorScheme; - final selected = value == groupValue; - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - leading: Container(width: 44, height: 44, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: colors.onSurface.withValues(alpha: 0.6), size: 22)), - title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), - subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), - trailing: selected ? Icon(Icons.check_circle, color: colors.onSurface, size: 20) : Icon(Icons.circle_outlined, color: colors.onSurface.withValues(alpha: 0.15), size: 20), - onTap: onTap, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface)), + ), + SegmentedButton( + segments: segments, + selected: {selected}, + onSelectionChanged: (s) => onChanged(s.first), + showSelectedIcon: false, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((s) => s.contains(WidgetState.selected) ? colors.onSurface : colors.surfaceContainerHighest), + foregroundColor: WidgetStateProperty.resolveWith((s) => s.contains(WidgetState.selected) ? colors.surface : colors.onSurface.withValues(alpha: 0.6)), + side: WidgetStateProperty.all(BorderSide.none), + shape: WidgetStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + padding: WidgetStateProperty.all(const EdgeInsets.symmetric(vertical: 10)), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.standard, + ), + ), + ], + ), ); } } diff --git a/lib/utils/sync/backup_service.dart b/lib/utils/sync/backup_service.dart index 0012446..1388cd1 100644 --- a/lib/utils/sync/backup_service.dart +++ b/lib/utils/sync/backup_service.dart @@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:share_plus/share_plus.dart'; +import 'package:sqflite/sqflite.dart'; import '../database_helper.dart'; import '../user_prefs.dart'; @@ -428,6 +429,16 @@ class BackupService { final data = backupData['data'] as Map; final db = await DatabaseHelper.instance.database; + // 预获取各表的列名(用于过滤不存在的列) + final moviesCols = await _getTableColumns(db, 'movies'); + final booksCols = await _getTableColumns(db, 'books'); + final notesCols = await _getTableColumns(db, 'notes'); + final movieReviewsCols = await _getTableColumns(db, 'movie_reviews'); + final moviePostersCols = await _getTableColumns(db, 'movie_posters'); + final bookReviewsCols = await _getTableColumns(db, 'book_reviews'); + final bookExcerptsCols = await _getTableColumns(db, 'book_excerpts'); + final tagsCols = await _getTableColumns(db, 'tags'); + // 开始事务 await db.transaction((txn) async { // 清空现有数据 @@ -439,50 +450,50 @@ class BackupService { await txn.delete('books'); await txn.delete('notes'); await txn.delete('tags'); - + // 导入影视数据(更新图片路径) if (data.containsKey('movies')) { final movies = data['movies'] as List; for (final movie in movies) { - final movieMap = _convertToDbMap(movie); + final movieMap = _convertToDbMapSafe(movie, moviesCols); final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap); await txn.insert('movies', updatedMap); } } - + // 导入书籍数据(更新图片路径) if (data.containsKey('books')) { final books = data['books'] as List; for (final book in books) { - final bookMap = _convertToDbMap(book); + final bookMap = _convertToDbMapSafe(book, booksCols); final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap); await txn.insert('books', updatedMap); } } - + // 导入笔记数据(更新图片路径) if (data.containsKey('notes')) { final notes = data['notes'] as List; for (final note in notes) { - final noteMap = _convertToDbMap(note); + final noteMap = _convertToDbMapSafe(note, notesCols); final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap); await txn.insert('notes', updatedMap); } } - + // 导入影评数据 if (data.containsKey('movie_reviews')) { final reviews = data['movie_reviews'] as List; for (final review in reviews) { - await txn.insert('movie_reviews', _convertToDbMap(review)); + await txn.insert('movie_reviews', _convertToDbMapSafe(review, movieReviewsCols)); } } - + // 导入海报墙数据(更新图片路径) if (data.containsKey('movie_posters')) { final posters = data['movie_posters'] as List; for (final poster in posters) { - final posterMap = _convertToDbMap(poster); + final posterMap = _convertToDbMapSafe(poster, moviePostersCols); final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap); await txn.insert('movie_posters', updatedMap); } @@ -491,14 +502,14 @@ class BackupService { if (data.containsKey('book_reviews')) { final reviews = data['book_reviews'] as List; for (final review in reviews) { - await txn.insert('book_reviews', _convertToDbMap(review)); + await txn.insert('book_reviews', _convertToDbMapSafe(review, bookReviewsCols)); } } // 导入书摘数据 if (data.containsKey('book_excerpts')) { final excerpts = data['book_excerpts'] as List; for (final excerpt in excerpts) { - await txn.insert('book_excerpts', _convertToDbMap(excerpt)); + await txn.insert('book_excerpts', _convertToDbMapSafe(excerpt, bookExcerptsCols)); } } @@ -506,7 +517,7 @@ class BackupService { if (data.containsKey('tags')) { final tags = data['tags'] as List; for (final tag in tags) { - final map = _convertToDbMap(tag); + final map = _convertToDbMapSafe(tag, tagsCols); await txn.rawInsert( 'INSERT OR IGNORE INTO tags (id, name, type, created_at) VALUES (?, ?, ?, ?)', [map['id'], map['name'], map['type'], map['created_at']], @@ -617,6 +628,16 @@ class BackupService { final data = backupData['data'] as Map; final db = await DatabaseHelper.instance.database; + // 预获取各表的列名 + final moviesCols = await _getTableColumns(db, 'movies'); + final booksCols = await _getTableColumns(db, 'books'); + final notesCols = await _getTableColumns(db, 'notes'); + final movieReviewsCols = await _getTableColumns(db, 'movie_reviews'); + final moviePostersCols = await _getTableColumns(db, 'movie_posters'); + final bookReviewsCols = await _getTableColumns(db, 'book_reviews'); + final bookExcerptsCols = await _getTableColumns(db, 'book_excerpts'); + final tagsCols = await _getTableColumns(db, 'tags'); + await db.transaction((txn) async { // 清空现有数据 await txn.delete('movie_reviews'); @@ -630,7 +651,7 @@ class BackupService { if (data.containsKey('movies')) { final movies = data['movies'] as List; for (final movie in movies) { - final movieMap = _convertToDbMap(movie); + final movieMap = _convertToDbMapSafe(movie, moviesCols); final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap); await txn.insert('movies', updatedMap); } @@ -639,7 +660,7 @@ class BackupService { if (data.containsKey('books')) { final books = data['books'] as List; for (final book in books) { - final bookMap = _convertToDbMap(book); + final bookMap = _convertToDbMapSafe(book, booksCols); final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap); await txn.insert('books', updatedMap); } @@ -648,7 +669,7 @@ class BackupService { if (data.containsKey('notes')) { final notes = data['notes'] as List; for (final note in notes) { - final noteMap = _convertToDbMap(note); + final noteMap = _convertToDbMapSafe(note, notesCols); final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap); await txn.insert('notes', updatedMap); } @@ -657,14 +678,14 @@ class BackupService { if (data.containsKey('movie_reviews')) { final reviews = data['movie_reviews'] as List; for (final review in reviews) { - await txn.insert('movie_reviews', _convertToDbMap(review)); + await txn.insert('movie_reviews', _convertToDbMapSafe(review, movieReviewsCols)); } } if (data.containsKey('movie_posters')) { final posters = data['movie_posters'] as List; for (final poster in posters) { - final posterMap = _convertToDbMap(poster); + final posterMap = _convertToDbMapSafe(poster, moviePostersCols); final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap); await txn.insert('movie_posters', updatedMap); } @@ -673,14 +694,14 @@ class BackupService { if (data.containsKey('book_reviews')) { final reviews = data['book_reviews'] as List; for (final review in reviews) { - await txn.insert('book_reviews', _convertToDbMap(review)); + await txn.insert('book_reviews', _convertToDbMapSafe(review, bookReviewsCols)); } } // 导入书摘数据 if (data.containsKey('book_excerpts')) { final excerpts = data['book_excerpts'] as List; for (final excerpt in excerpts) { - await txn.insert('book_excerpts', _convertToDbMap(excerpt)); + await txn.insert('book_excerpts', _convertToDbMapSafe(excerpt, bookExcerptsCols)); } } @@ -688,7 +709,7 @@ class BackupService { if (data.containsKey('tags')) { final tags = data['tags'] as List; for (final tag in tags) { - final map = _convertToDbMap(tag); + final map = _convertToDbMapSafe(tag, tagsCols); await txn.rawInsert( 'INSERT OR IGNORE INTO tags (id, name, type, created_at) VALUES (?, ?, ?, ?)', [map['id'], map['name'], map['type'], map['created_at']], @@ -736,6 +757,19 @@ class BackupService { } } + /// 将动态类型转换为数据库可用的 Map,并过滤掉表中不存在的列 + Map _convertToDbMapSafe(dynamic item, Set validColumns) { + final raw = _convertToDbMap(item); + if (raw.isEmpty) return raw; + return Map.fromEntries(raw.entries.where((e) => validColumns.contains(e.key))); + } + + /// 获取表的列名集合 + Future> _getTableColumns(Database db, String table) async { + final columns = await db.rawQuery('PRAGMA table_info($table)'); + return columns.map((c) => c['name'] as String).toSet(); + } + /// 将动态类型转换为数据库可用的 Map Map _convertToDbMap(dynamic item) { if (item is Map) {