From 901d6e27d64069b95ce46fbf1046f8ad44afbe9e Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Sat, 7 Mar 2026 23:57:36 +0800 Subject: [PATCH] =?UTF-8?q?ok=E4=BA=86=E8=80=81=E9=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/data_models_extension.dart | 64 +++++-- lib/pages/note_form_page.dart | 85 ++++++++-- lib/pages/webdav_sync_page.dart | 4 +- lib/utils/webdav_service.dart | 6 +- lib/widgets/custom_drawer.dart | 235 ++++++++++++++++++++++++++ 5 files changed, 361 insertions(+), 33 deletions(-) diff --git a/lib/models/data_models_extension.dart b/lib/models/data_models_extension.dart index 0b131a6..ff08a95 100644 --- a/lib/models/data_models_extension.dart +++ b/lib/models/data_models_extension.dart @@ -9,22 +9,36 @@ extension MovieExtension on Movie { Movie copyWith({ String? id, String? title, - String? poster, + String? posterPath, + DateTime? releaseDate, + List? directors, + List? writers, + List? actors, + List? genres, + List? alternateTitles, + String? summary, double? rating, - int? year, String? status, - DateTime? watchDate, - String? note, + DateTime? createdAt, + DateTime? updatedAt, + bool? isDeleted, }) { return Movie( id: id ?? this.id, title: title ?? this.title, - poster: poster ?? this.poster, + posterPath: posterPath ?? this.posterPath, + releaseDate: releaseDate ?? this.releaseDate, + directors: directors ?? this.directors, + writers: writers ?? this.writers, + actors: actors ?? this.actors, + genres: genres ?? this.genres, + alternateTitles: alternateTitles ?? this.alternateTitles, + summary: summary ?? this.summary, rating: rating ?? this.rating, - year: year ?? this.year, status: status ?? this.status, - watchDate: watchDate ?? this.watchDate, - note: note ?? this.note, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + isDeleted: isDeleted ?? this.isDeleted, ); } } @@ -35,22 +49,32 @@ extension BookExtension on Book { Book copyWith({ String? id, String? title, - String? author, - String? cover, + String? coverPath, + List? authors, + List? alternateTitles, + String? publisher, + List? genres, + String? summary, double? rating, String? status, - DateTime? readDate, - String? note, + DateTime? createdAt, + DateTime? updatedAt, + bool? isDeleted, }) { return Book( id: id ?? this.id, title: title ?? this.title, - author: author ?? this.author, - cover: cover ?? this.cover, + coverPath: coverPath ?? this.coverPath, + authors: authors ?? this.authors, + alternateTitles: alternateTitles ?? this.alternateTitles, + publisher: publisher ?? this.publisher, + genres: genres ?? this.genres, + summary: summary ?? this.summary, rating: rating ?? this.rating, status: status ?? this.status, - readDate: readDate ?? this.readDate, - note: note ?? this.note, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + isDeleted: isDeleted ?? this.isDeleted, ); } } @@ -60,19 +84,23 @@ extension NoteExtension on Note { /// 创建副本并允许修改部分属性 Note copyWith({ String? id, - String? title, String? content, + String? contentType, List? tags, + List? images, DateTime? createdAt, DateTime? updatedAt, + bool? isDeleted, }) { return Note( id: id ?? this.id, - title: title ?? this.title, content: content ?? this.content, + contentType: contentType ?? this.contentType, tags: tags ?? this.tags, + images: images ?? this.images, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + isDeleted: isDeleted ?? this.isDeleted, ); } } diff --git a/lib/pages/note_form_page.dart b/lib/pages/note_form_page.dart index f9ba192..6b62372 100644 --- a/lib/pages/note_form_page.dart +++ b/lib/pages/note_form_page.dart @@ -410,6 +410,12 @@ class _NoteFormPageState extends State { void _showAddTagDialog() { final controller = TextEditingController(); + // 获取所有已有标签(从所有笔记中收集) + final provider = context.read(); + final allTags = _getAllExistingTags(provider); + // 过滤掉已添加的标签 + final availableTags = allTags.where((tag) => !_tags.contains(tag)).toList(); + showDialog( context: context, builder: (context) => AlertDialog( @@ -423,17 +429,67 @@ class _NoteFormPageState extends State { fontWeight: FontWeight.w600, ), ), - content: TextField( - controller: controller, - autofocus: true, - decoration: const InputDecoration( - hintText: '输入标签名称', - border: UnderlineInputBorder(), + content: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 输入框 + TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration( + hintText: '输入新标签名称', + border: UnderlineInputBorder(), + ), + onSubmitted: (value) { + _addTag(value); + Navigator.pop(context); + }, + ), + + // 已有标签列表 + if (availableTags.isNotEmpty) ...[ + const SizedBox(height: 16), + const Text( + '或选择已有标签:', + style: TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: availableTags.map((tag) { + return GestureDetector( + onTap: () { + _addTag(tag); + Navigator.pop(context); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + tag, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF666666), + ), + ), + ), + ); + }).toList(), + ), + ], + ], ), - onSubmitted: (value) { - _addTag(value); - Navigator.pop(context); - }, ), actions: [ TextButton( @@ -451,6 +507,15 @@ class _NoteFormPageState extends State { ), ); } + + /// 获取所有已有标签(从所有笔记中收集) + List _getAllExistingTags(AppProvider provider) { + final allTags = {}; + for (final note in provider.notes) { + allTags.addAll(note.tags); + } + return allTags.toList()..sort(); + } /// 添加标签 void _addTag(String tag) { diff --git a/lib/pages/webdav_sync_page.dart b/lib/pages/webdav_sync_page.dart index 06bc04a..e9aa51d 100644 --- a/lib/pages/webdav_sync_page.dart +++ b/lib/pages/webdav_sync_page.dart @@ -53,7 +53,7 @@ class _WebDAVSyncPageState extends State { try { if (value) { await WebDAVService.instance.startAutoSync(); - ToastUtil.show(context, '自动备份已开启,每2分钟执行一次'); + ToastUtil.show(context, '自动备份已开启,每5分钟执行一次'); } else { await WebDAVService.instance.stopAutoSync(); ToastUtil.show(context, '自动备份已关闭'); @@ -386,7 +386,7 @@ class _WebDAVSyncPageState extends State { ), SizedBox(height: 2), Text( - '每2分钟自动备份一次,保留最近10个备份', + '每5分钟自动备份一次,保留最近10个备份', style: TextStyle( fontSize: 12, color: Color(0xFF999999), diff --git a/lib/utils/webdav_service.dart b/lib/utils/webdav_service.dart index 98fa5df..14758c5 100644 --- a/lib/utils/webdav_service.dart +++ b/lib/utils/webdav_service.dart @@ -60,7 +60,7 @@ class WebDAVService { static const String _backupListKey = 'webdav_backup_list'; static const int _maxBackupCount = 10; // 保留最近10条备份 - static const Duration _autoSyncInterval = Duration(minutes: 2); // 每2分钟自动备份 + static const Duration _autoSyncInterval = Duration(minutes: 5); // 每5分钟自动备份 Map? _cachedConfig; Timer? _autoSyncTimer; @@ -403,7 +403,7 @@ class WebDAVService { } }); - print('WebDAV: 自动备份已启动,每2分钟执行一次'); + print('WebDAV: 自动备份已启动,每5分钟执行一次'); } /// 停止自动同步 @@ -551,7 +551,7 @@ class WebDAVService { final bytes = await entity.readAsBytes(); final archivePath = '$relativePath/$fileName'; archive.addFile(ArchiveFile(archivePath, bytes.length, bytes)); - print('WebDAV: 添加文件到备份 - $archivePath'); + // print('WebDAV: 添加文件到备份 - $archivePath'); } else if (entity is Directory) { final dirName = p.basename(entity.path); await _addImagesToArchive(archive, entity, '$relativePath/$dirName'); diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index f06475e..b205091 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../utils/user_prefs.dart'; import '../utils/toast_util.dart'; +import '../models/data_models.dart'; /// 自定义左侧弹出菜单 - 极简主义设计 class CustomDrawer extends StatelessWidget { @@ -20,6 +22,11 @@ class CustomDrawer extends StatelessWidget { const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + // 回顾功能区域 + _buildMemorySection(context), + + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + // 菜单项列表 Expanded( child: ListView( @@ -44,6 +51,219 @@ class CustomDrawer extends StatelessWidget { ); } + /// 构建回顾功能区域 + Widget _buildMemorySection(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + final memoryItem = _getRandomMemoryItem(provider); + + if (memoryItem == null) { + return const SizedBox.shrink(); + } + + final memoryText = _buildMemoryText(memoryItem); + final timeAgo = _getTimeAgoText(memoryItem.date); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + Row( + children: [ + const Icon( + Icons.history, + size: 16, + color: Color(0xFF666666), + ), + const SizedBox(width: 8), + const Text( + '回顾', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF666666), + ), + ), + ], + ), + const SizedBox(height: 12), + + // 内容卡片(带头图) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 头图(影视/书籍显示,笔记不显示) + if (memoryItem.imagePath != null && memoryItem.imagePath!.isNotEmpty) + Container( + width: 60, + height: 80, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(4), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.file( + File(memoryItem.imagePath!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Icon( + Icons.image, + color: Color(0xFFCCCCCC), + ), + ), + ), + ) + else if (memoryItem.type != 'note') + Container( + width: 60, + height: 80, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + memoryItem.type == 'movie' ? Icons.movie : Icons.menu_book, + color: const Color(0xFFCCCCCC), + size: 24, + ), + ), + + // 文字内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 时间标签 + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + timeAgo, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF999999), + ), + ), + ), + const SizedBox(height: 8), + + // 内容文字 + Text( + memoryText, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + height: 1.5, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ], + ), + ); + }, + ); + } + + /// 获取随机回顾项 + _MemoryItem? _getRandomMemoryItem(AppProvider provider) { + final now = DateTime.now(); + final candidates = <_MemoryItem>[]; + + // 收集所有未删除的影视、书籍(去掉笔记) + for (final movie in provider.movies.where((m) => !m.isDeleted)) { + candidates.add(_MemoryItem( + type: 'movie', + title: movie.title, + date: movie.createdAt, + imagePath: movie.posterPath, + )); + } + + for (final book in provider.books.where((b) => !b.isDeleted)) { + candidates.add(_MemoryItem( + type: 'book', + title: book.title, + date: book.createdAt, + imagePath: book.coverPath, + )); + } + + if (candidates.isEmpty) return null; + + // 优先选择1个月、3个月、6个月、1年前的数据 + final oneMonthAgo = now.subtract(const Duration(days: 30)); + final threeMonthsAgo = now.subtract(const Duration(days: 90)); + final sixMonthsAgo = now.subtract(const Duration(days: 180)); + final oneYearAgo = now.subtract(const Duration(days: 365)); + + final memoryCandidates = candidates.where((item) { + return _isInTimeRange(item.date, oneMonthAgo, threeMonthsAgo, sixMonthsAgo, oneYearAgo); + }).toList(); + + // 如果有符合时间范围的,从中随机选择;否则从所有数据中随机选择 + final random = Random(); + final selectedList = memoryCandidates.isNotEmpty ? memoryCandidates : candidates; + return selectedList[random.nextInt(selectedList.length)]; + } + + /// 检查时间是否在范围内(1月、3月、6月、1年前) + bool _isInTimeRange(DateTime date, DateTime oneMonth, DateTime threeMonths, + DateTime sixMonths, DateTime oneYear) { + // 检查是否在1个月前左右(±7天) + if (_isCloseTo(date, oneMonth)) return true; + // 检查是否在3个月前左右(±14天) + if (_isCloseTo(date, threeMonths, days: 14)) return true; + // 检查是否在6个月前左右(±30天) + if (_isCloseTo(date, sixMonths, days: 30)) return true; + // 检查是否在1年前左右(±30天) + if (_isCloseTo(date, oneYear, days: 30)) return true; + return false; + } + + /// 检查两个日期是否接近 + bool _isCloseTo(DateTime date, DateTime target, {int days = 7}) { + final diff = date.difference(target).inDays.abs(); + return diff <= days; + } + + /// 构建回顾文本 + String _buildMemoryText(_MemoryItem item) { + // 只显示标题,不添加描述前缀 + return '《${item.title}》'; + } + + /// 获取时间描述文本 + String _getTimeAgoText(DateTime date) { + final now = DateTime.now(); + final diff = now.difference(date); + + if (diff.inDays >= 365) { + return '1年前'; + } else if (diff.inDays >= 180) { + return '6个月前'; + } else if (diff.inDays >= 90) { + return '3个月前'; + } else if (diff.inDays >= 30) { + return '1个月前'; + } else { + return '${diff.inDays}天前'; + } + } + /// 构建头部 Widget _buildHeader(BuildContext context) { return Consumer( @@ -192,3 +412,18 @@ class CustomDrawer extends StatelessWidget { ToastUtil.show(context, message); } } + +/// 回顾项数据类 +class _MemoryItem { + final String type; // 'movie', 'book', 'note' + final String title; + final DateTime date; + final String? imagePath; // 头图路径(影视/书籍有,笔记无) + + _MemoryItem({ + required this.type, + required this.title, + required this.date, + this.imagePath, + }); +}