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({
String? id,
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,
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<String>? authors,
List<String>? alternateTitles,
String? publisher,
List<String>? 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<String>? tags,
List<String>? 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,
);
}
}

View File

@@ -410,6 +410,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
void _showAddTagDialog() {
final controller = TextEditingController();
// 获取所有已有标签(从所有笔记中收集)
final provider = context.read<AppProvider>();
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<NoteFormPage> {
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<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) {

View File

@@ -53,7 +53,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
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<WebDAVSyncPage> {
),
SizedBox(height: 2),
Text(
'2分钟自动备份一次保留最近10个备份',
'5分钟自动备份一次保留最近10个备份',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),

View File

@@ -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<String, String>? _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');

View File

@@ -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<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) {
return Consumer<AppProvider>(
@@ -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,
});
}