ok了老铁

This commit is contained in:
DelLevin-Home
2026-03-07 23:57:36 +08:00
parent d28c5e562e
commit 901d6e27d6
5 changed files with 361 additions and 33 deletions

View File

@@ -9,22 +9,36 @@ extension MovieExtension on Movie {
Movie copyWith({ Movie copyWith({
String? id, String? id,
String? title, String? title,
String? poster, String? posterPath,
DateTime? releaseDate,
List<String>? directors,
List<String>? writers,
List<String>? actors,
List<String>? genres,
List<String>? alternateTitles,
String? summary,
double? rating, double? rating,
int? year,
String? status, String? status,
DateTime? watchDate, DateTime? createdAt,
String? note, DateTime? updatedAt,
bool? isDeleted,
}) { }) {
return Movie( return Movie(
id: id ?? this.id, id: id ?? this.id,
title: title ?? this.title, 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, rating: rating ?? this.rating,
year: year ?? this.year,
status: status ?? this.status, status: status ?? this.status,
watchDate: watchDate ?? this.watchDate, createdAt: createdAt ?? this.createdAt,
note: note ?? this.note, updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
); );
} }
} }
@@ -35,22 +49,32 @@ extension BookExtension on Book {
Book copyWith({ Book copyWith({
String? id, String? id,
String? title, String? title,
String? author, String? coverPath,
String? cover, List<String>? authors,
List<String>? alternateTitles,
String? publisher,
List<String>? genres,
String? summary,
double? rating, double? rating,
String? status, String? status,
DateTime? readDate, DateTime? createdAt,
String? note, DateTime? updatedAt,
bool? isDeleted,
}) { }) {
return Book( return Book(
id: id ?? this.id, id: id ?? this.id,
title: title ?? this.title, title: title ?? this.title,
author: author ?? this.author, coverPath: coverPath ?? this.coverPath,
cover: cover ?? this.cover, authors: authors ?? this.authors,
alternateTitles: alternateTitles ?? this.alternateTitles,
publisher: publisher ?? this.publisher,
genres: genres ?? this.genres,
summary: summary ?? this.summary,
rating: rating ?? this.rating, rating: rating ?? this.rating,
status: status ?? this.status, status: status ?? this.status,
readDate: readDate ?? this.readDate, createdAt: createdAt ?? this.createdAt,
note: note ?? this.note, updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
); );
} }
} }
@@ -60,19 +84,23 @@ extension NoteExtension on Note {
/// 创建副本并允许修改部分属性 /// 创建副本并允许修改部分属性
Note copyWith({ Note copyWith({
String? id, String? id,
String? title,
String? content, String? content,
String? contentType,
List<String>? tags, List<String>? tags,
List<String>? images,
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
bool? isDeleted,
}) { }) {
return Note( return Note(
id: id ?? this.id, id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content, content: content ?? this.content,
contentType: contentType ?? this.contentType,
tags: tags ?? this.tags, tags: tags ?? this.tags,
images: images ?? this.images,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted,
); );
} }
} }

View File

@@ -410,6 +410,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
void _showAddTagDialog() { void _showAddTagDialog() {
final controller = TextEditingController(); final controller = TextEditingController();
// 获取所有已有标签(从所有笔记中收集)
final provider = context.read<AppProvider>();
final allTags = _getAllExistingTags(provider);
// 过滤掉已添加的标签
final availableTags = allTags.where((tag) => !_tags.contains(tag)).toList();
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@@ -423,17 +429,67 @@ class _NoteFormPageState extends State<NoteFormPage> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
content: TextField( content: SizedBox(
controller: controller, width: double.maxFinite,
autofocus: true, child: Column(
decoration: const InputDecoration( mainAxisSize: MainAxisSize.min,
hintText: '输入标签名称', crossAxisAlignment: CrossAxisAlignment.start,
border: UnderlineInputBorder(), 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: [ actions: [
TextButton( TextButton(
@@ -451,6 +507,15 @@ class _NoteFormPageState extends State<NoteFormPage> {
), ),
); );
} }
/// 获取所有已有标签(从所有笔记中收集)
List<String> _getAllExistingTags(AppProvider provider) {
final allTags = <String>{};
for (final note in provider.notes) {
allTags.addAll(note.tags);
}
return allTags.toList()..sort();
}
/// 添加标签 /// 添加标签
void _addTag(String tag) { void _addTag(String tag) {

View File

@@ -53,7 +53,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
try { try {
if (value) { if (value) {
await WebDAVService.instance.startAutoSync(); await WebDAVService.instance.startAutoSync();
ToastUtil.show(context, '自动备份已开启,每2分钟执行一次'); ToastUtil.show(context, '自动备份已开启,每5分钟执行一次');
} else { } else {
await WebDAVService.instance.stopAutoSync(); await WebDAVService.instance.stopAutoSync();
ToastUtil.show(context, '自动备份已关闭'); ToastUtil.show(context, '自动备份已关闭');
@@ -386,7 +386,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
), ),
SizedBox(height: 2), SizedBox(height: 2),
Text( Text(
'2分钟自动备份一次保留最近10个备份', '5分钟自动备份一次保留最近10个备份',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF999999), color: Color(0xFF999999),

View File

@@ -60,7 +60,7 @@ class WebDAVService {
static const String _backupListKey = 'webdav_backup_list'; static const String _backupListKey = 'webdav_backup_list';
static const int _maxBackupCount = 10; // 保留最近10条备份 static const int _maxBackupCount = 10; // 保留最近10条备份
static const Duration _autoSyncInterval = Duration(minutes: 2); // 每2分钟自动备份 static const Duration _autoSyncInterval = Duration(minutes: 5); // 每5分钟自动备份
Map<String, String>? _cachedConfig; Map<String, String>? _cachedConfig;
Timer? _autoSyncTimer; 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 bytes = await entity.readAsBytes();
final archivePath = '$relativePath/$fileName'; final archivePath = '$relativePath/$fileName';
archive.addFile(ArchiveFile(archivePath, bytes.length, bytes)); archive.addFile(ArchiveFile(archivePath, bytes.length, bytes));
print('WebDAV: 添加文件到备份 - $archivePath'); // print('WebDAV: 添加文件到备份 - $archivePath');
} else if (entity is Directory) { } else if (entity is Directory) {
final dirName = p.basename(entity.path); final dirName = p.basename(entity.path);
await _addImagesToArchive(archive, entity, '$relativePath/$dirName'); await _addImagesToArchive(archive, entity, '$relativePath/$dirName');

View File

@@ -1,9 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../utils/user_prefs.dart'; import '../utils/user_prefs.dart';
import '../utils/toast_util.dart'; import '../utils/toast_util.dart';
import '../models/data_models.dart';
/// 自定义左侧弹出菜单 - 极简主义设计 /// 自定义左侧弹出菜单 - 极简主义设计
class CustomDrawer extends StatelessWidget { class CustomDrawer extends StatelessWidget {
@@ -20,6 +22,11 @@ class CustomDrawer extends StatelessWidget {
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), 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( Expanded(
child: ListView( child: ListView(
@@ -44,6 +51,219 @@ class CustomDrawer extends StatelessWidget {
); );
} }
/// 构建回顾功能区域
Widget _buildMemorySection(BuildContext context) {
return Consumer<AppProvider>(
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) { Widget _buildHeader(BuildContext context) {
return Consumer<AppProvider>( return Consumer<AppProvider>(
@@ -192,3 +412,18 @@ class CustomDrawer extends StatelessWidget {
ToastUtil.show(context, message); 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,
});
}