From 25d2efda6ae7eb9e48a827aa19e0c62519f86106 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Fri, 3 Jul 2026 01:33:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=91=E5=A4=87=E4=BB=BD=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/book/book_detail_page.dart | 6 + lib/pages/book/book_form_page.dart | 1 + lib/pages/book/book_tab_page.dart | 6 +- lib/pages/sync/cloud_sync_page.dart | 9 + lib/pages/sync/webdav_sync_page.dart | 431 ++++++++++++--------------- lib/utils/sync/webdav_service.dart | 52 +++- lib/widgets/add_sheet.dart | 3 +- lib/widgets/book_status_bar.dart | 4 +- lib/widgets/note_list_item.dart | 90 +++--- 9 files changed, 305 insertions(+), 297 deletions(-) diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index 4828307..40bd45b 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -580,6 +580,7 @@ class _BookDetailPageState extends State { 'read' => ('已读', colors.primary, colors.onPrimary), 'reading' => ('在读', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)), 'want_to_read' => ('想读', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)), + 'abandoned' => ('弃读', colors.error.withValues(alpha: 0.15), colors.error), _ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)), }; if (label.isEmpty) return const SizedBox.shrink(); @@ -702,6 +703,11 @@ class _BookDetailPageState extends State { bgColor = colors.surfaceContainerHighest; textColor = colors.onSurface.withValues(alpha: 0.4); break; + case 'abandoned': + label = '弃读'; + bgColor = colors.error.withValues(alpha: 0.15); + textColor = colors.error; + break; default: label = '未知'; bgColor = colors.outlineVariant; diff --git a/lib/pages/book/book_form_page.dart b/lib/pages/book/book_form_page.dart index 28ad946..b250600 100644 --- a/lib/pages/book/book_form_page.dart +++ b/lib/pages/book/book_form_page.dart @@ -266,6 +266,7 @@ class _BookFormPageState extends State { _buildStatusOption('想读', 'want_to_read'), _buildStatusOption('在读', 'reading'), _buildStatusOption('已读', 'read'), + _buildStatusOption('弃读', 'abandoned'), ]), ), ]), diff --git a/lib/pages/book/book_tab_page.dart b/lib/pages/book/book_tab_page.dart index d2ace0b..be6d6c9 100644 --- a/lib/pages/book/book_tab_page.dart +++ b/lib/pages/book/book_tab_page.dart @@ -44,7 +44,7 @@ class _BookTabPageState extends State { } } - static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'}; + static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read', 3: 'abandoned'}; @override void initState() { @@ -166,7 +166,7 @@ class _BookTabPageState extends State { final direction = (velocity ?? 0) > 0 ? -1 : 1; // 右滑→上一个,左滑→下一个 final provider = context.read(); final currentIndex = provider.bookStatusIndex; - final newIndex = (currentIndex + direction + 3) % 3; + final newIndex = (currentIndex + direction + 4) % 4; setState(() => _dragDelta = 0.0); provider.setBookStatusIndex(newIndex); }, @@ -297,7 +297,7 @@ class _BookTabPageState extends State { final colors = Theme.of(context).colorScheme; final provider = context.read(); final isWallMode = provider.bookshelfMode; - final statusText = isWallMode ? '' : ['已读', '在读', '想读'][statusIndex]; + final statusText = isWallMode ? '' : ['已读', '在读', '想读', '弃读'][statusIndex]; return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Container(width: 80, height: 80, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)), diff --git a/lib/pages/sync/cloud_sync_page.dart b/lib/pages/sync/cloud_sync_page.dart index 67366b2..22932cd 100644 --- a/lib/pages/sync/cloud_sync_page.dart +++ b/lib/pages/sync/cloud_sync_page.dart @@ -24,6 +24,15 @@ class CloudSyncPage extends StatelessWidget { onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())), ), + const SizedBox(height: 12), + _buildOption( + colors: colors, + icon: Icons.cloud_sync_outlined, + title: '服务器云同步', + subtitle: '自建云同步服务器,多端同步更改', + onTap: () {}, + enabled: false, + ), const SizedBox(height: 20), _buildInfo(colors), ], diff --git a/lib/pages/sync/webdav_sync_page.dart b/lib/pages/sync/webdav_sync_page.dart index a4d8898..634c4ad 100644 --- a/lib/pages/sync/webdav_sync_page.dart +++ b/lib/pages/sync/webdav_sync_page.dart @@ -21,10 +21,13 @@ class _WebDAVSyncPageState extends State { bool _isLoading = false; bool _isConfigured = false; bool _obscurePassword = true; - bool _isAutoSyncEnabled = false; - int _autoSyncInterval = 5; SyncDirection _syncDirection = SyncDirection.upload; + // 远程备份信息 + DateTime? _remoteModifiedTime; + int? _remoteFileSize; + bool _isLoadingRemoteInfo = false; + @override void initState() { super.initState(); @@ -50,14 +53,24 @@ class _WebDAVSyncPageState extends State { _pathController.text = config['path'] ?? '/mooknote'; _isConfigured = true; }); + _loadRemoteInfo(); } final autoSyncEnabled = await WebDAVService.instance.isAutoSyncEnabled(); final autoSyncInterval = await WebDAVService.instance.getAutoSyncInterval(); - setState(() { - _isAutoSyncEnabled = autoSyncEnabled; - _autoSyncInterval = autoSyncInterval; - }); + debugPrint('WebDAV auto sync: enabled=$autoSyncEnabled, interval=$autoSyncInterval'); + } + + Future _loadRemoteInfo() async { + setState(() => _isLoadingRemoteInfo = true); + final info = await WebDAVService.instance.getRemoteBackupInfo(); + if (mounted) { + setState(() { + _remoteModifiedTime = info?['modifiedTime'] as DateTime?; + _remoteFileSize = info?['size'] as int?; + _isLoadingRemoteInfo = false; + }); + } } Future _saveConfig() async { @@ -118,6 +131,8 @@ class _WebDAVSyncPageState extends State { if (!mounted) return; if (result.success) { + // 同步成功后刷新远程文件信息 + _loadRemoteInfo(); final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n' '下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片'; @@ -189,76 +204,79 @@ class _WebDAVSyncPageState extends State { ); } - Future _toggleAutoSync(bool value) async { - setState(() => _isLoading = true); - try { - if (value) { - await WebDAVService.instance.startAutoSync(); - if (mounted) ToastUtil.show(context, '自动同步已开启,每 $_autoSyncInterval 分钟同步一次'); - } else { - await WebDAVService.instance.stopAutoSync(); - if (mounted) ToastUtil.show(context, '自动同步已关闭'); - } - setState(() => _isAutoSyncEnabled = value); - } catch (e) { - if (mounted) ToastUtil.show(context, '设置失败: $e'); - } finally { - if (mounted) setState(() => _isLoading = false); - } - } - - Future _showIntervalPicker() async { - final intervals = [1, 3, 5, 10, 15, 30, 60]; - final selected = await showModalBottomSheet( + Future _showUploadConfirm() async { + final confirmed = await showDialog( context: context, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (ctx) { final colors = Theme.of(ctx).colorScheme; - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - Container( - width: 36, - height: 4, - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))), - const SizedBox(height: 12), - Text('同步间隔', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)), - const SizedBox(height: 8), - ...intervals.map((i) => ListTile( - title: Text('$i 分钟', - style: TextStyle( - fontSize: 15, - fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400, - color: colors.onSurface)), - trailing: _autoSyncInterval == i - ? Icon(Icons.check, color: colors.primary, size: 20) - : null, - onTap: () => Navigator.pop(ctx, i), - )), - const SizedBox(height: 8), - ], - ), + return 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('该操作会覆盖掉远程数据,请谨慎操作,点击确定继续上传', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, + foregroundColor: colors.onPrimary, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('确定'), + ), + ], ); }, ); + if (confirmed == true) { + setState(() => _syncDirection = SyncDirection.upload); + _syncData(); + } + } - if (selected != null && selected != _autoSyncInterval) { - setState(() => _isLoading = true); - try { - await WebDAVService.instance.setAutoSyncInterval(selected); - setState(() => _autoSyncInterval = selected); - if (mounted) ToastUtil.show(context, '同步间隔已设置为 $selected 分钟'); - } catch (e) { - if (mounted) ToastUtil.show(context, '设置失败: $e'); - } finally { - if (mounted) setState(() => _isLoading = false); - } + Future _showDownloadConfirm() async { + final confirmed = await showDialog( + context: context, + builder: (ctx) { + final colors = Theme.of(ctx).colorScheme; + return 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('该操作会拉取远程数据覆盖掉本地数据,请谨慎操作,点击确定将数据拉取到本地', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, + foregroundColor: colors.onPrimary, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('确定'), + ), + ], + ); + }, + ); + if (confirmed == true) { + setState(() => _syncDirection = SyncDirection.download); + _syncData(); } } @@ -325,7 +343,6 @@ class _WebDAVSyncPageState extends State { _passwordController.clear(); _pathController.text = '/mooknote'; _isConfigured = false; - _isAutoSyncEnabled = false; }); if (mounted) ToastUtil.show(context, '配置已清除'); } @@ -339,18 +356,18 @@ class _WebDAVSyncPageState extends State { return Scaffold( backgroundColor: colors.surface, appBar: AppBar(title: const Text('WebDAV 备份')), - body: _isLoading && !_isConfigured - ? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)) - : ListView( - padding: const EdgeInsets.symmetric(horizontal: 20), - children: [ - const SizedBox(height: 4), + body: Stack( + children: [ + ListView( + padding: const EdgeInsets.symmetric(horizontal: 20), + children: [ + const SizedBox(height: 4), - // 已连接提示 - if (_isConfigured) _buildConnectedBanner(colors), + // 已连接提示 + if (_isConfigured) _buildConnectedBanner(colors), - // 服务器配置 - _buildSectionLabel(colors, '服务器配置'), + // 服务器配置 + _buildSectionLabel(colors, '服务器配置'), const SizedBox(height: 12), _buildInput( colors: colors, @@ -391,61 +408,39 @@ class _WebDAVSyncPageState extends State { // 测试并保存 _buildBtn(colors, '测试并保存', onTap: _isLoading ? null : _saveConfig), + if (_isConfigured) ...[ + const SizedBox(height: 8), + Center( + child: GestureDetector( + onTap: _isLoading ? null : _clearConfig, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text('清除配置', + style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))), + ), + ), + ), + ], const SizedBox(height: 28), if (_isConfigured) ...[ - // 手动同步 - _buildSectionLabel(colors, '手动同步'), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildDirectionChip( - colors, '上传到云端', SyncDirection.upload, Icons.upload)), - const SizedBox(width: 12), - Expanded( - child: _buildDirectionChip( - colors, '下载到本地', SyncDirection.download, Icons.download)), - ], - ), - const SizedBox(height: 16), - _buildBtn(colors, '立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading), - + // 远程备份信息卡片(包含操作按钮) + _buildRemoteInfoCard(colors), const SizedBox(height: 24), - - // 自动同步 - _buildSectionLabel(colors, '自动同步'), - const SizedBox(height: 10), - _buildSwitchRow( - colors: colors, - icon: Icons.sync, - label: '自动同步', - sub: _isAutoSyncEnabled ? '每 $_autoSyncInterval 分钟自动同步' : '关闭', - value: _isAutoSyncEnabled, - onChanged: _isLoading ? null : _toggleAutoSync, - ), - if (_isAutoSyncEnabled) ...[ - const SizedBox(height: 4), - _buildTappableRow( - colors: colors, - label: '同步间隔', - value: '$_autoSyncInterval 分钟', - onTap: _isLoading ? null : _showIntervalPicker, - ), - ], - - const SizedBox(height: 24), - - // 清除配置 - _buildTextBtn(colors, '清除配置', onTap: _isLoading ? null : _clearConfig), - const SizedBox(height: 4), ], - const SizedBox(height: 24), - _buildTips(colors), - const SizedBox(height: 40), - ], + const SizedBox(height: 24), + _buildTips(colors), + const SizedBox(height: 40), + ], + ), + if (_isLoading) + Container( + color: colors.surface.withValues(alpha: 0.7), + child: Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)), ), + ], + ), ); } @@ -558,126 +553,92 @@ class _WebDAVSyncPageState extends State { ); } - Widget _buildTextBtn(ColorScheme colors, String text, {VoidCallback? onTap}) { - return GestureDetector( - onTap: onTap, - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - text, - style: TextStyle( - fontSize: 14, - color: onTap == null - ? colors.onSurface.withValues(alpha: 0.25) - : colors.onSurface.withValues(alpha: 0.4))), - ), - ), - ); - } + Widget _buildRemoteInfoCard(ColorScheme colors) { + String timeText; + String sizeText = ''; + + if (_isLoadingRemoteInfo) { + timeText = '加载中...'; + } else if (_remoteModifiedTime != null) { + final dt = _remoteModifiedTime!; + timeText = '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + if (_remoteFileSize != null) { + sizeText = _formatFileSize(_remoteFileSize!); + } + } else { + timeText = '暂无备份文件'; + } - Widget _buildSwitchRow({ - required ColorScheme colors, - required IconData icon, - required String label, - required String sub, - required bool value, - required ValueChanged? onChanged, - }) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.5), width: 0.5), ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, - size: 18, - color: value ? colors.primary : colors.onSurface.withValues(alpha: 0.3)), - const SizedBox(width: 10), - Expanded( - child: Text( - value ? sub : label, - style: TextStyle( - fontSize: 13, - color: value ? colors.onSurface.withValues(alpha: 0.6) : colors.onSurface), - ), + Row( + children: [ + Icon(Icons.cloud_outlined, size: 18, color: colors.primary), + const SizedBox(width: 8), + Text('云端备份', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)), + const Spacer(), + if (_isLoadingRemoteInfo) + SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)), + ], ), - Switch( - value: value, - onChanged: onChanged, - activeThumbColor: colors.primary, - activeTrackColor: colors.primary.withValues(alpha: 0.3), - inactiveThumbColor: colors.surface, - inactiveTrackColor: colors.onSurface.withValues(alpha: 0.15), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('上传时间', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), + const SizedBox(height: 4), + Text(timeText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + ], + ), + ), + if (sizeText.isNotEmpty) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('文件大小', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))), + const SizedBox(height: 4), + Text(sizeText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(height: 0.5, color: Color(0xFFE0E0E0)), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildBtn(colors, '上传', onTap: _isLoading ? null : () => _showUploadConfirm(), loading: _isLoading), + ), + const SizedBox(width: 12), + Expanded( + child: _buildBtn(colors, '下载', onTap: _isLoading ? null : () => _showDownloadConfirm(), loading: _isLoading), + ), + ], ), ], ), ); } - Widget _buildTappableRow({ - required ColorScheme colors, - required String label, - required String value, - VoidCallback? onTap, - }) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const SizedBox(width: 28), - Text(label, - style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))), - const Spacer(), - Text(value, - style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))), - const SizedBox(width: 4), - Icon(Icons.chevron_right, - size: 16, color: colors.onSurface.withValues(alpha: 0.25)), - ], - ), - ), - ); - } - - Widget _buildDirectionChip(ColorScheme colors, String label, SyncDirection dir, IconData icon) { - final selected = _syncDirection == dir; - return GestureDetector( - onTap: () => setState(() => _syncDirection = dir), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: selected ? colors.primary : colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, - size: 16, - color: selected - ? colors.onPrimary - : colors.onSurface.withValues(alpha: 0.4)), - const SizedBox(width: 6), - Text(label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: selected - ? colors.onPrimary - : colors.onSurface.withValues(alpha: 0.6))), - ], - ), - ), - ); + String _formatFileSize(int bytes) { + if (bytes < 1024) return '${bytes}B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB'; + if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; } Widget _buildTips(ColorScheme colors) { diff --git a/lib/utils/sync/webdav_service.dart b/lib/utils/sync/webdav_service.dart index cb010b2..ec872b2 100644 --- a/lib/utils/sync/webdav_service.dart +++ b/lib/utils/sync/webdav_service.dart @@ -490,7 +490,57 @@ class WebDAVService { } } - /// 获取远程文件的修改时间 + /// 获取远程备份文件信息(修改时间和大小) + Future?> getRemoteBackupInfo() async { + final config = await getConfig(); + if (config == null) return null; + + final url = config['url']!; + final username = config['username']!; + final password = config['password']!; + final path = config['path']!; + + final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; + final zipUrl = '$baseUrl$path/mooknote_backup.zip'; + + final client = http.Client(); + try { + var request = http.Request('HEAD', Uri.parse(zipUrl)); + request.headers['Authorization'] = _basicAuth(username, password); + + var response = await client.send(request); + + // 处理重定向 + if (response.statusCode == 301 || response.statusCode == 302 || + response.statusCode == 307 || response.statusCode == 308) { + final location = response.headers['location']; + if (location != null) { + request = http.Request('HEAD', Uri.parse(location)); + request.headers['Authorization'] = _basicAuth(username, password); + response = await client.send(request); + } + } + + if (response.statusCode == 200) { + final lastModified = response.headers['last-modified']; + final contentLength = response.headers['content-length']; + DateTime? modifiedTime; + if (lastModified != null) { + modifiedTime = HttpDate.parse(lastModified).toLocal(); + } + return { + 'modifiedTime': modifiedTime, + 'size': contentLength != null ? int.tryParse(contentLength) : null, + }; + } + return null; + } catch (e) { + debugPrint('[WebDAV] 获取远程备份信息失败: $e'); + return null; + } finally { + client.close(); + } + } Future _getRemoteFileModifiedTime( http.Client client, String url, diff --git a/lib/widgets/add_sheet.dart b/lib/widgets/add_sheet.dart index 56b7835..414b6ec 100644 --- a/lib/widgets/add_sheet.dart +++ b/lib/widgets/add_sheet.dart @@ -13,7 +13,7 @@ void showQuickAddSheet(BuildContext context, AppProvider provider) { Navigator.pushNamed(context, '/movie-form', arguments: {'initialStatus': currentStatus}); case 1: - final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'}; + final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read', 3: 'abandoned'}; final currentStatus = statusMap[provider.bookStatusIndex] ?? 'want_to_read'; Navigator.pushNamed(context, '/book-form', @@ -93,6 +93,7 @@ void showAddSheet(BuildContext context, AppProvider provider) { 0: 'read', 1: 'reading', 2: 'want_to_read', + 3: 'abandoned', }; final s = statusMap[provider.bookStatusIndex] ?? 'want_to_read'; diff --git a/lib/widgets/book_status_bar.dart b/lib/widgets/book_status_bar.dart index ff2e882..2cc66fc 100644 --- a/lib/widgets/book_status_bar.dart +++ b/lib/widgets/book_status_bar.dart @@ -26,7 +26,7 @@ class BookStatusBar extends StatelessWidget { ), child: LayoutBuilder( builder: (context, constraints) { - final tabWidth = constraints.maxWidth / 3; + final tabWidth = constraints.maxWidth / 4; return SizedBox( height: 40, child: Stack( @@ -57,6 +57,8 @@ class BookStatusBar extends StatelessWidget { () => provider.setBookStatusIndex(1)), _buildTab(colors, '想读', Icons.bookmark_outlined, currentIndex == 2, () => provider.setBookStatusIndex(2)), + _buildTab(colors, '弃读', Icons.cancel_outlined, currentIndex == 3, + () => provider.setBookStatusIndex(3)), ], ), ], diff --git a/lib/widgets/note_list_item.dart b/lib/widgets/note_list_item.dart index 81f80d1..093843b 100644 --- a/lib/widgets/note_list_item.dart +++ b/lib/widgets/note_list_item.dart @@ -39,75 +39,53 @@ class _NoteListItemContent extends StatelessWidget { }, onLongPress: () => _showActions(context), child: Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(14), + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(10), border: selected ? Border.all(color: colors.primary, width: 1.5) : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - // 标题(带置顶图标) - if (note.title.isNotEmpty) ...[ - const SizedBox(height: 6), - Row( - children: [ - if (note.isPinned) - Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon(Icons.push_pin, size: 14, color: colors.primary), - ), - Expanded( - child: Text( - note.title, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: colors.onSurface, - height: 1.3, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + // 标题行(带置顶图标 + 时间) + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (note.isPinned) + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon(Icons.push_pin, size: 14, color: colors.primary), ), - const SizedBox(width: 12), - Text( - _formatDate(note.createdAt), + Expanded( + child: Text( + note.title.isNotEmpty ? note.title : previewText.isNotEmpty ? previewText : '(无内容)', style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.35), + fontSize: 15, + fontWeight: FontWeight.w600, + color: colors.onSurface, + height: 1.3, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ], - ), - ], + ), + const SizedBox(width: 12), + Text( + _formatDate(note.createdAt), + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.35), + ), + ), + ], + ), - // 内容预览(无标题时才显示置顶和时间,替换原本只有置顶的逻辑) + // 内容预览 if (previewText.isNotEmpty) ...[ const SizedBox(height: 6), - if (note.title.isEmpty) - Row( - children: [ - if (note.isPinned) - Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon(Icons.push_pin, size: 14, color: colors.primary), - ), - const Spacer(), - Text( - _formatDate(note.createdAt), - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.35), - ), - ), - ], - ), - if (note.title.isEmpty) - const SizedBox(height: 2), Text( previewText, style: TextStyle( @@ -115,12 +93,12 @@ class _NoteListItemContent extends StatelessWidget { color: colors.onSurface.withValues(alpha: 0.5), height: 1.5, ), - maxLines: 3, + maxLines: note.title.isNotEmpty ? 2 : 3, overflow: TextOverflow.ellipsis, ), ], - // 标签(换行显示) + // 标签 if (note.tags.isNotEmpty) ...[ const SizedBox(height: 8), Wrap(