generated from dellevin/template
优化设置页面功能
This commit is contained in:
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))),
|
|
||||||
),
|
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))),
|
|
||||||
),
|
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user