优化设置页面功能

This commit is contained in:
DelLevin-Home
2026-06-20 14:28:17 +08:00
parent c7f74c61f5
commit 3c45c10a75
7 changed files with 233 additions and 191 deletions

View File

@@ -141,12 +141,9 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: colors.surface, backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0,
title: Row(children: [ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
Icon(Icons.system_update_outlined, color: colors.primary, size: 24), title: Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(width: 10),
Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
]),
content: Text('新版本 $version 已发布,是否下载更新?', content: Text('新版本 $version 已发布,是否下载更新?',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)), style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [ actions: [
@@ -155,10 +152,9 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
UserPrefs().setDismissedVersion(version); UserPrefs().setDismissedVersion(version);
Navigator.pop(ctx); 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), ElevatedButton(
FilledButton.icon(
onPressed: () async { onPressed: () async {
Navigator.pop(ctx); Navigator.pop(ctx);
try { try {
@@ -171,11 +167,15 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
} }
} }
}, },
icon: const Icon(Icons.open_in_browser, size: 18), style: ElevatedButton.styleFrom(
label: const Text('去官网下载'), 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),
), ),
); );
} }

View File

@@ -232,15 +232,6 @@ class _BookTabPageState extends State<BookTabPage> {
child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), child: Icon(Icons.menu_book_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 20), const SizedBox(height: 20),
Text('暂无$statusText的书籍', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), 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))),
),
])); ]));
} }
} }

View File

@@ -47,27 +47,43 @@ class _ChangelogPageState extends State<ChangelogPage> {
localVersion: localVersion, localVersion: localVersion,
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( _showNoUpdateDialog(localVersion);
content: Text('已是最新版本(当前 $localVersion'),
duration: const Duration(seconds: 2),
));
} }
} catch (_) {} } catch (_) {}
if (mounted) setState(() => _checking = false); 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}) { void _showUpdateDialog({required String version, String? localVersion}) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: colors.surface, backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0,
title: Row(children: [ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
Icon(Icons.system_update_outlined, color: colors.primary, size: 24), title: Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(width: 10),
Text('发现新版本', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
]),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -84,21 +100,24 @@ class _ChangelogPageState extends State<ChangelogPage> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), 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), ElevatedButton(
FilledButton.icon(
onPressed: () async { onPressed: () async {
Navigator.pop(ctx); Navigator.pop(ctx);
try { try {
await launchUrl(Uri.parse(_websiteUrl), mode: LaunchMode.externalApplication); await launchUrl(Uri.parse(_websiteUrl), mode: LaunchMode.externalApplication);
} catch (_) {} } catch (_) {}
}, },
icon: const Icon(Icons.open_in_browser, size: 18), style: ElevatedButton.styleFrom(
label: const Text('去官网下载'), 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),
), ),
); );
} }

View File

@@ -309,18 +309,6 @@ class _MovieTabPageState extends State<MovieTabPage> {
child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), child: Icon(Icons.movie_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 20), const SizedBox(height: 20),
Text('暂无$statusText的影片', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), 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)),
),
),
])); ]));
} }
} }

View File

@@ -322,12 +322,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
child: Icon(Icons.note_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))), child: Icon(Icons.note_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 20), const SizedBox(height: 20),
Text('暂无笔记', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))), 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))),
),
])); ]));
} }
} }

View File

@@ -136,7 +136,7 @@ class _ProfilePageState extends State<ProfilePage> {
: Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)), : Icon(Icons.person_outline, size: 32, color: colors.onSurface.withValues(alpha: 0.25)),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -181,7 +181,7 @@ class _ProfilePageState extends State<ProfilePage> {
children: [ children: [
Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.3)), Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.3)),
const SizedBox(width: 8), 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(), const Spacer(),
Text(count, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), Text(count, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
], ],
@@ -243,7 +243,7 @@ class _ProfilePageState extends State<ProfilePage> {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), 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<ProfilePage> {
const SizedBox(height: 16), const SizedBox(height: 16),
ListTile( ListTile(
contentPadding: EdgeInsets.zero, 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))), 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: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), title: Text('本地备份', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text('备份到本地文件夹,支持恢复', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), 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)), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); }, onTap: () { Navigator.pop(ctx); _push(context, const BackupPage()); },
), ),
Divider(height: 0.5, color: colors.outlineVariant), Divider(height: 0.5, color: colors.outlineVariant),
ListTile( ListTile(
contentPadding: EdgeInsets.zero, 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))), 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: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), title: Text('云备份', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text('通过 WebDAV 同步到云端', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), 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)), trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); }, onTap: () { Navigator.pop(ctx); _push(context, const CloudSyncPage()); },
), ),
@@ -450,22 +450,7 @@ class _SettingsPageState extends State<SettingsPage> {
appBar: AppBar(title: const Text('设置')), appBar: AppBar(title: const Text('设置')),
body: ListView( body: ListView(
children: [ children: [
_buildSectionHeader('数据管理'), _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),
_buildNavigationItem( _buildNavigationItem(
icon: Icons.apps_outlined, icon: Icons.apps_outlined,
title: '应用图标', title: '应用图标',
@@ -483,14 +468,37 @@ class _SettingsPageState extends State<SettingsPage> {
_buildNavigationItem( _buildNavigationItem(
icon: Icons.dashboard_outlined, icon: Icons.dashboard_outlined,
title: '布局设置', title: '布局设置',
subtitle: '笔记、影视、阅读的展示样式', subtitle: '影视、阅读、笔记的展示样式',
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LayoutSettingsPage())),
), ),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
_buildSectionHeader('外观'),
_buildThemeModeSelector(), _buildThemeModeSelector(),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), 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('帮助'), _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( _buildLinkItem(
context: context, context: context,
icon: Icons.help_outline, icon: Icons.help_outline,
@@ -499,13 +507,6 @@ class _SettingsPageState extends State<SettingsPage> {
url: 'https://mooknote.iletter.top/#/guide', url: 'https://mooknote.iletter.top/#/guide',
), ),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), 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<SettingsPage> {
return InkWell( return InkWell(
onTap: () => _showThemeModePicker(), onTap: () => _showThemeModePicker(),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row( child: Row(
children: [ 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)), 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: 16), const SizedBox(width: 12),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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), Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
@@ -560,8 +561,8 @@ class _SettingsPageState extends State<SettingsPage> {
const SizedBox(height: 16), const SizedBox(height: 16),
for (int i = 0; i < _themeModeLabels.length; i++) for (int i = 0; i < _themeModeLabels.length; i++)
ListTile( 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))), 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: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), 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, trailing: _themeMode == i ? Icon(Icons.check, color: colors.onSurface, size: 20) : null,
onTap: () async { await _setThemeMode(i); Navigator.pop(ctx); }, onTap: () async { await _setThemeMode(i); Navigator.pop(ctx); },
), ),
@@ -590,16 +591,16 @@ class _SettingsPageState extends State<SettingsPage> {
return InkWell( return InkWell(
onTap: () => onChanged(!value), onTap: () => onChanged(!value),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row( child: Row(
children: [ 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)), 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: 16), const SizedBox(width: 12),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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), 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<SettingsPage> {
Widget _buildSectionHeader(String title) { Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 12), padding: const EdgeInsets.fromLTRB(24, 20, 24, 8),
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), child: Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface)),
); );
} }
@@ -622,16 +623,16 @@ class _SettingsPageState extends State<SettingsPage> {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row( child: Row(
children: [ 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)), 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: 16), const SizedBox(width: 12),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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), Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
@@ -646,16 +647,16 @@ class _SettingsPageState extends State<SettingsPage> {
return InkWell( return InkWell(
onTap: () => _launchUrl(context, url), onTap: () => _launchUrl(context, url),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row( child: Row(
children: [ 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)), 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: 16), const SizedBox(width: 12),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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), Icon(Icons.open_in_new, color: colors.onSurface.withValues(alpha: 0.25), size: 18),
@@ -670,16 +671,16 @@ class _SettingsPageState extends State<SettingsPage> {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row( child: Row(
children: [ 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)), 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: 16), const SizedBox(width: 12),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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<SettingsPage> {
context: pageContext, context: pageContext,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
backgroundColor: colors.surface, elevation: 0, backgroundColor: colors.surface, elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text('清除缓存数据'), title: Text('清除缓存数据', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: const Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?'), content: Text('这将删除所有未在数据库中引用的图片文件。确定要继续吗?',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
TextButton(onPressed: () async { ElevatedButton(
Navigator.pop(dialogContext); onPressed: () async {
await _clearCacheData(pageContext); Navigator.pop(dialogContext);
}, child: Text('确定', style: TextStyle(color: colors.error))), 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<SettingsPage> {
final imagesDir = Directory('${appDir.path}/images'); 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)) { 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 (_) {} try { await entity.delete(); deletedCount++; } catch (_) {}
} }
} }
@@ -830,7 +839,7 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), 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<MainContentSettingsPage> {
Widget _buildSectionHeader(String title) { Widget _buildSectionHeader(String title) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), padding: const EdgeInsets.fromLTRB(24, 20, 24, 8),
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), child: Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface)),
); );
} }
@@ -849,9 +858,9 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), 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)), 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: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), title: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), 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), 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<MainContentSettingsPage> {
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), 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)), 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: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), title: Text('默认启动标签', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), subtitle: Text('打开应用时默认显示的页面', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -895,8 +904,8 @@ class _MainContentSettingsPageState extends State<MainContentSettingsPage> {
const SizedBox(height: 16), const SizedBox(height: 16),
for (int i = 0; i < labels.length; i++) for (int i = 0; i < labels.length; i++)
ListTile( 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))), 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: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), 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, trailing: _defaultTabIndex == i ? Icon(Icons.check, color: colors.onSurface, 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); },
), ),
@@ -938,24 +947,21 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
backgroundColor: colors.surface, backgroundColor: colors.surface,
appBar: AppBar(title: const Text('布局设置')), appBar: AppBar(title: const Text('布局设置')),
body: ListView( body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [ children: [
_buildSectionHeader('笔记布局'), _buildSection('影视布局', [
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列列表,简洁清晰', value: 0, groupValue: _noteLayout, onTap: () => _setLayout('note', 0)), ButtonSegment(value: 0, icon: Icon(Icons.grid_view_outlined, size: 16), label: Text('海报网格', style: TextStyle(fontSize: 12))),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), ButtonSegment(value: 1, icon: Icon(Icons.view_list_outlined, size: 16), label: Text('列表', style: TextStyle(fontSize: 12))),
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '瀑布流布局', subtitle: '双列卡片,图文并茂', value: 1, groupValue: _noteLayout, onTap: () => _setLayout('note', 1)), ], _movieLayout, (v) => _setLayout('movie', v)),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), _buildSection('阅读布局', [
_buildLayoutOption(icon: Icons.timeline_outlined, title: '时间线布局', subtitle: '按时间排列,纵览全局', value: 2, groupValue: _noteLayout, onTap: () => _setLayout('note', 2)), ButtonSegment(value: 0, icon: Icon(Icons.grid_view_outlined, size: 16), label: Text('封面网格', style: TextStyle(fontSize: 12))),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), ButtonSegment(value: 1, icon: Icon(Icons.view_list_outlined, size: 16), label: Text('列表', style: TextStyle(fontSize: 12))),
_buildSectionHeader('影视布局'), ], _bookLayout, (v) => _setLayout('book', v)),
_buildLayoutOption(icon: Icons.grid_view_outlined, title: '海报网格', subtitle: '三列海报,赏心悦目', value: 0, groupValue: _movieLayout, onTap: () => _setLayout('movie', 0)), _buildSection('笔记布局', [
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), ButtonSegment(value: 0, icon: Icon(Icons.view_list_outlined, size: 16), label: Text('列表', style: TextStyle(fontSize: 12))),
_buildLayoutOption(icon: Icons.view_list_outlined, title: '列表布局', subtitle: '单列卡片,信息一览', value: 1, groupValue: _movieLayout, onTap: () => _setLayout('movie', 1)), ButtonSegment(value: 1, icon: Icon(Icons.grid_view_outlined, size: 16), label: Text('瀑布流', style: TextStyle(fontSize: 12))),
Divider(height: 0.5, indent: 24, endIndent: 24, color: colors.outlineVariant), ButtonSegment(value: 2, icon: Icon(Icons.timeline_outlined, size: 16), label: Text('时间线', style: TextStyle(fontSize: 12))),
_buildSectionHeader('阅读布局'), ], _noteLayout, (v) => _setLayout('note', v)),
_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),
], ],
), ),
); );
@@ -969,24 +975,34 @@ class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
} }
} }
Widget _buildSectionHeader(String title) { Widget _buildSection(String title, List<ButtonSegment<int>> segments, int selected, ValueChanged<int> onChanged) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), child: Column(
); crossAxisAlignment: CrossAxisAlignment.start,
} children: [
Padding(
Widget _buildLayoutOption({required IconData icon, required String title, required String subtitle, required int value, required int groupValue, required VoidCallback onTap}) { padding: const EdgeInsets.only(left: 4, bottom: 8),
final colors = Theme.of(context).colorScheme; child: Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: colors.onSurface)),
final selected = value == groupValue; ),
return ListTile( SegmentedButton<int>(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), segments: segments,
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)), selected: {selected},
title: Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)), onSelectionChanged: (s) => onChanged(s.first),
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), showSelectedIcon: false,
trailing: selected ? Icon(Icons.check_circle, color: colors.onSurface, size: 20) : Icon(Icons.circle_outlined, color: colors.onSurface.withValues(alpha: 0.15), size: 20), style: ButtonStyle(
onTap: onTap, 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,
),
),
],
),
); );
} }
} }

View File

@@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:sqflite/sqflite.dart';
import '../database_helper.dart'; import '../database_helper.dart';
import '../user_prefs.dart'; import '../user_prefs.dart';
@@ -428,6 +429,16 @@ class BackupService {
final data = backupData['data'] as Map<String, dynamic>; final data = backupData['data'] as Map<String, dynamic>;
final db = await DatabaseHelper.instance.database; 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 db.transaction((txn) async {
// 清空现有数据 // 清空现有数据
@@ -439,50 +450,50 @@ class BackupService {
await txn.delete('books'); await txn.delete('books');
await txn.delete('notes'); await txn.delete('notes');
await txn.delete('tags'); await txn.delete('tags');
// 导入影视数据(更新图片路径) // 导入影视数据(更新图片路径)
if (data.containsKey('movies')) { if (data.containsKey('movies')) {
final movies = data['movies'] as List<dynamic>; final movies = data['movies'] as List<dynamic>;
for (final movie in movies) { for (final movie in movies) {
final movieMap = _convertToDbMap(movie); final movieMap = _convertToDbMapSafe(movie, moviesCols);
final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap); final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap);
await txn.insert('movies', updatedMap); await txn.insert('movies', updatedMap);
} }
} }
// 导入书籍数据(更新图片路径) // 导入书籍数据(更新图片路径)
if (data.containsKey('books')) { if (data.containsKey('books')) {
final books = data['books'] as List<dynamic>; final books = data['books'] as List<dynamic>;
for (final book in books) { for (final book in books) {
final bookMap = _convertToDbMap(book); final bookMap = _convertToDbMapSafe(book, booksCols);
final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap); final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap);
await txn.insert('books', updatedMap); await txn.insert('books', updatedMap);
} }
} }
// 导入笔记数据(更新图片路径) // 导入笔记数据(更新图片路径)
if (data.containsKey('notes')) { if (data.containsKey('notes')) {
final notes = data['notes'] as List<dynamic>; final notes = data['notes'] as List<dynamic>;
for (final note in notes) { for (final note in notes) {
final noteMap = _convertToDbMap(note); final noteMap = _convertToDbMapSafe(note, notesCols);
final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap); final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap);
await txn.insert('notes', updatedMap); await txn.insert('notes', updatedMap);
} }
} }
// 导入影评数据 // 导入影评数据
if (data.containsKey('movie_reviews')) { if (data.containsKey('movie_reviews')) {
final reviews = data['movie_reviews'] as List<dynamic>; final reviews = data['movie_reviews'] as List<dynamic>;
for (final review in reviews) { 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')) { if (data.containsKey('movie_posters')) {
final posters = data['movie_posters'] as List<dynamic>; final posters = data['movie_posters'] as List<dynamic>;
for (final poster in posters) { for (final poster in posters) {
final posterMap = _convertToDbMap(poster); final posterMap = _convertToDbMapSafe(poster, moviePostersCols);
final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap); final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap);
await txn.insert('movie_posters', updatedMap); await txn.insert('movie_posters', updatedMap);
} }
@@ -491,14 +502,14 @@ class BackupService {
if (data.containsKey('book_reviews')) { if (data.containsKey('book_reviews')) {
final reviews = data['book_reviews'] as List<dynamic>; final reviews = data['book_reviews'] as List<dynamic>;
for (final review in reviews) { 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')) { if (data.containsKey('book_excerpts')) {
final excerpts = data['book_excerpts'] as List<dynamic>; final excerpts = data['book_excerpts'] as List<dynamic>;
for (final excerpt in excerpts) { 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')) { if (data.containsKey('tags')) {
final tags = data['tags'] as List<dynamic>; final tags = data['tags'] as List<dynamic>;
for (final tag in tags) { for (final tag in tags) {
final map = _convertToDbMap(tag); final map = _convertToDbMapSafe(tag, tagsCols);
await txn.rawInsert( await txn.rawInsert(
'INSERT OR IGNORE INTO tags (id, name, type, created_at) VALUES (?, ?, ?, ?)', 'INSERT OR IGNORE INTO tags (id, name, type, created_at) VALUES (?, ?, ?, ?)',
[map['id'], map['name'], map['type'], map['created_at']], [map['id'], map['name'], map['type'], map['created_at']],
@@ -617,6 +628,16 @@ class BackupService {
final data = backupData['data'] as Map<String, dynamic>; final data = backupData['data'] as Map<String, dynamic>;
final db = await DatabaseHelper.instance.database; 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 db.transaction((txn) async {
// 清空现有数据 // 清空现有数据
await txn.delete('movie_reviews'); await txn.delete('movie_reviews');
@@ -630,7 +651,7 @@ class BackupService {
if (data.containsKey('movies')) { if (data.containsKey('movies')) {
final movies = data['movies'] as List<dynamic>; final movies = data['movies'] as List<dynamic>;
for (final movie in movies) { for (final movie in movies) {
final movieMap = _convertToDbMap(movie); final movieMap = _convertToDbMapSafe(movie, moviesCols);
final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap); final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap);
await txn.insert('movies', updatedMap); await txn.insert('movies', updatedMap);
} }
@@ -639,7 +660,7 @@ class BackupService {
if (data.containsKey('books')) { if (data.containsKey('books')) {
final books = data['books'] as List<dynamic>; final books = data['books'] as List<dynamic>;
for (final book in books) { for (final book in books) {
final bookMap = _convertToDbMap(book); final bookMap = _convertToDbMapSafe(book, booksCols);
final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap); final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap);
await txn.insert('books', updatedMap); await txn.insert('books', updatedMap);
} }
@@ -648,7 +669,7 @@ class BackupService {
if (data.containsKey('notes')) { if (data.containsKey('notes')) {
final notes = data['notes'] as List<dynamic>; final notes = data['notes'] as List<dynamic>;
for (final note in notes) { for (final note in notes) {
final noteMap = _convertToDbMap(note); final noteMap = _convertToDbMapSafe(note, notesCols);
final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap); final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap);
await txn.insert('notes', updatedMap); await txn.insert('notes', updatedMap);
} }
@@ -657,14 +678,14 @@ class BackupService {
if (data.containsKey('movie_reviews')) { if (data.containsKey('movie_reviews')) {
final reviews = data['movie_reviews'] as List<dynamic>; final reviews = data['movie_reviews'] as List<dynamic>;
for (final review in reviews) { 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')) { if (data.containsKey('movie_posters')) {
final posters = data['movie_posters'] as List<dynamic>; final posters = data['movie_posters'] as List<dynamic>;
for (final poster in posters) { for (final poster in posters) {
final posterMap = _convertToDbMap(poster); final posterMap = _convertToDbMapSafe(poster, moviePostersCols);
final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap); final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap);
await txn.insert('movie_posters', updatedMap); await txn.insert('movie_posters', updatedMap);
} }
@@ -673,14 +694,14 @@ class BackupService {
if (data.containsKey('book_reviews')) { if (data.containsKey('book_reviews')) {
final reviews = data['book_reviews'] as List<dynamic>; final reviews = data['book_reviews'] as List<dynamic>;
for (final review in reviews) { 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')) { if (data.containsKey('book_excerpts')) {
final excerpts = data['book_excerpts'] as List<dynamic>; final excerpts = data['book_excerpts'] as List<dynamic>;
for (final excerpt in excerpts) { 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')) { if (data.containsKey('tags')) {
final tags = data['tags'] as List<dynamic>; final tags = data['tags'] as List<dynamic>;
for (final tag in tags) { for (final tag in tags) {
final map = _convertToDbMap(tag); final map = _convertToDbMapSafe(tag, tagsCols);
await txn.rawInsert( await txn.rawInsert(
'INSERT OR IGNORE INTO tags (id, name, type, created_at) VALUES (?, ?, ?, ?)', 'INSERT OR IGNORE INTO tags (id, name, type, created_at) VALUES (?, ?, ?, ?)',
[map['id'], map['name'], map['type'], map['created_at']], [map['id'], map['name'], map['type'], map['created_at']],
@@ -736,6 +757,19 @@ class BackupService {
} }
} }
/// 将动态类型转换为数据库可用的 Map并过滤掉表中不存在的列
Map<String, dynamic> _convertToDbMapSafe(dynamic item, Set<String> validColumns) {
final raw = _convertToDbMap(item);
if (raw.isEmpty) return raw;
return Map.fromEntries(raw.entries.where((e) => validColumns.contains(e.key)));
}
/// 获取表的列名集合
Future<Set<String>> _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
Map<String, dynamic> _convertToDbMap(dynamic item) { Map<String, dynamic> _convertToDbMap(dynamic item) {
if (item is Map<String, dynamic>) { if (item is Map<String, dynamic>) {