generated from dellevin/template
基本功能完善
This commit is contained in:
@@ -12,15 +12,41 @@ class AppRouter {
|
||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case '/movie-form':
|
||||
final movie = settings.arguments as Movie?;
|
||||
// 处理不同参数类型:Movie 对象或 Map(包含 initialStatus)
|
||||
final args = settings.arguments;
|
||||
Movie? movie;
|
||||
String? initialStatus;
|
||||
|
||||
if (args is Movie) {
|
||||
movie = args;
|
||||
} else if (args is Map<String, dynamic>) {
|
||||
initialStatus = args['initialStatus'] as String?;
|
||||
}
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => MovieFormPage(movie: movie),
|
||||
builder: (_) => MovieFormPage(
|
||||
movie: movie,
|
||||
initialStatus: initialStatus,
|
||||
),
|
||||
);
|
||||
|
||||
case '/book-form':
|
||||
final book = settings.arguments as Book?;
|
||||
// 处理不同参数类型:Book 对象或 Map(包含 initialStatus)
|
||||
final args = settings.arguments;
|
||||
Book? book;
|
||||
String? initialStatus;
|
||||
|
||||
if (args is Book) {
|
||||
book = args;
|
||||
} else if (args is Map<String, dynamic>) {
|
||||
initialStatus = args['initialStatus'] as String?;
|
||||
}
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => BookFormPage(book: book),
|
||||
builder: (_) => BookFormPage(
|
||||
book: book,
|
||||
initialStatus: initialStatus,
|
||||
),
|
||||
);
|
||||
|
||||
case '/note-form':
|
||||
|
||||
440
lib/utils/backup_service.dart
Normal file
440
lib/utils/backup_service.dart
Normal file
@@ -0,0 +1,440 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'database_helper.dart';
|
||||
|
||||
/// 数据备份服务 - 支持导出和导入数据(包含图片)
|
||||
class BackupService {
|
||||
static final BackupService instance = BackupService._init();
|
||||
|
||||
BackupService._init();
|
||||
|
||||
/// 导出所有数据和图片为 ZIP 文件,并选择保存路径
|
||||
Future<ExportResult> exportDataWithImages() async {
|
||||
try {
|
||||
final db = await DatabaseHelper.instance.database;
|
||||
|
||||
// 导出所有表的数据
|
||||
final movies = await db.query('movies');
|
||||
final books = await db.query('books');
|
||||
final notes = await db.query('notes');
|
||||
final movieReviews = await db.query('movie_reviews');
|
||||
final moviePosters = await db.query('movie_posters');
|
||||
|
||||
// 收集所有图片路径
|
||||
final imagePaths = <String>{};
|
||||
|
||||
// 收集影视海报
|
||||
for (final movie in movies) {
|
||||
final posterPath = movie['poster_path'] as String?;
|
||||
if (posterPath != null && posterPath.isNotEmpty) {
|
||||
imagePaths.add(posterPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 收集书籍封面
|
||||
for (final book in books) {
|
||||
final coverPath = book['cover_path'] as String?;
|
||||
if (coverPath != null && coverPath.isNotEmpty) {
|
||||
imagePaths.add(coverPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 收集海报墙图片
|
||||
for (final poster in moviePosters) {
|
||||
final posterPath = poster['poster_path'] as String?;
|
||||
if (posterPath != null && posterPath.isNotEmpty) {
|
||||
imagePaths.add(posterPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建备份数据
|
||||
final backupData = {
|
||||
'version': 2,
|
||||
'exportTime': DateTime.now().toIso8601String(),
|
||||
'appName': 'MookNote',
|
||||
'hasImages': true,
|
||||
'data': {
|
||||
'movies': movies,
|
||||
'books': books,
|
||||
'notes': notes,
|
||||
'movie_reviews': movieReviews,
|
||||
'movie_posters': moviePosters,
|
||||
},
|
||||
};
|
||||
|
||||
// 创建 ZIP 文件
|
||||
final archive = Archive();
|
||||
|
||||
// 添加 JSON 数据
|
||||
final jsonString = const JsonEncoder.withIndent(' ').convert(backupData);
|
||||
final jsonBytes = Uint8List.fromList(utf8.encode(jsonString));
|
||||
archive.addFile(ArchiveFile('data.json', jsonBytes.length, jsonBytes));
|
||||
|
||||
// 添加图片文件
|
||||
int imageCount = 0;
|
||||
for (final imagePath in imagePaths) {
|
||||
final file = File(imagePath);
|
||||
if (await file.exists()) {
|
||||
final bytes = await file.readAsBytes();
|
||||
final fileName = path.basename(imagePath);
|
||||
// 使用相对路径存储图片
|
||||
archive.addFile(ArchiveFile('images/$fileName', bytes.length, bytes));
|
||||
imageCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩 ZIP
|
||||
final zipEncoder = ZipEncoder();
|
||||
final zipBytes = zipEncoder.encode(archive);
|
||||
if (zipBytes == null) {
|
||||
return ExportResult.error('压缩备份文件失败');
|
||||
}
|
||||
|
||||
// 保存到临时目录
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final fileName = 'mooknote_backup_${_formatDateTime(DateTime.now())}.zip';
|
||||
final tempFilePath = path.join(tempDir.path, fileName);
|
||||
final tempFile = File(tempFilePath);
|
||||
await tempFile.writeAsBytes(zipBytes);
|
||||
|
||||
// 在移动端使用分享功能,让用户选择保存位置
|
||||
// 在桌面端可以尝试使用 saveFile
|
||||
String? finalPath;
|
||||
|
||||
try {
|
||||
// 尝试使用系统保存对话框(桌面端支持)
|
||||
final outputPath = await FilePicker.platform.saveFile(
|
||||
dialogTitle: '保存备份文件',
|
||||
fileName: fileName,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['zip'],
|
||||
bytes: Uint8List.fromList(zipBytes), // 在移动端需要提供 bytes
|
||||
);
|
||||
|
||||
if (outputPath == null) {
|
||||
// 用户取消,返回临时文件路径
|
||||
finalPath = tempFilePath;
|
||||
} else {
|
||||
finalPath = outputPath;
|
||||
// 如果保存路径不是临时文件路径,需要复制过去
|
||||
if (finalPath != tempFilePath) {
|
||||
final outputFile = File(finalPath);
|
||||
await outputFile.writeAsBytes(zipBytes);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果 saveFile 失败,使用临时文件路径
|
||||
finalPath = tempFilePath;
|
||||
}
|
||||
|
||||
return ExportResult.success(
|
||||
filePath: finalPath,
|
||||
movieCount: movies.length,
|
||||
bookCount: books.length,
|
||||
noteCount: notes.length,
|
||||
imageCount: imageCount,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExportResult.error('导出失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 分享备份文件
|
||||
Future<void> shareBackup(String filePath) async {
|
||||
final file = XFile(filePath);
|
||||
await Share.shareXFiles(
|
||||
[file],
|
||||
subject: 'MookNote 数据备份',
|
||||
text: '这是我的 MookNote 数据备份文件',
|
||||
);
|
||||
}
|
||||
|
||||
/// 选择并导入备份文件(支持 ZIP 格式)
|
||||
Future<ImportResult> importData() async {
|
||||
try {
|
||||
// 选择文件
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['zip', 'json'],
|
||||
allowMultiple: false,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return ImportResult.cancelled();
|
||||
}
|
||||
|
||||
final filePath = result.files.first.path;
|
||||
if (filePath == null) {
|
||||
return ImportResult.error('无法读取文件路径');
|
||||
}
|
||||
|
||||
final file = File(filePath);
|
||||
final extension = path.extension(filePath).toLowerCase();
|
||||
|
||||
Map<String, dynamic> backupData;
|
||||
int imageCount = 0;
|
||||
// 记录图片文件名到新路径的映射
|
||||
final imagePathMap = <String, String>{};
|
||||
|
||||
if (extension == '.zip') {
|
||||
// 处理 ZIP 文件
|
||||
final bytes = await file.readAsBytes();
|
||||
final archive = ZipDecoder().decodeBytes(bytes);
|
||||
|
||||
// 查找 data.json
|
||||
final dataFile = archive.findFile('data.json');
|
||||
if (dataFile == null) {
|
||||
return ImportResult.error('备份文件中没有找到数据文件');
|
||||
}
|
||||
|
||||
final jsonString = utf8.decode(dataFile.content as List<int>);
|
||||
backupData = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||
|
||||
// 解压图片到应用目录
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final imagesDir = Directory(path.join(appDir.path, 'images'));
|
||||
if (!await imagesDir.exists()) {
|
||||
await imagesDir.create(recursive: true);
|
||||
}
|
||||
|
||||
for (final archiveFile in archive) {
|
||||
if (archiveFile.name.startsWith('images/')) {
|
||||
final fileName = path.basename(archiveFile.name);
|
||||
final outputFile = File(path.join(imagesDir.path, fileName));
|
||||
await outputFile.writeAsBytes(archiveFile.content as List<int>);
|
||||
imagePathMap[fileName] = outputFile.path;
|
||||
imageCount++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 处理旧版 JSON 文件
|
||||
final jsonString = await file.readAsString();
|
||||
backupData = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
// 验证备份格式
|
||||
if (!backupData.containsKey('data')) {
|
||||
return ImportResult.error('无效的备份文件格式');
|
||||
}
|
||||
|
||||
// 导入数据
|
||||
final data = backupData['data'] as Map<String, dynamic>;
|
||||
final db = await DatabaseHelper.instance.database;
|
||||
|
||||
// 开始事务
|
||||
await db.transaction((txn) async {
|
||||
// 清空现有数据
|
||||
await txn.delete('movie_reviews');
|
||||
await txn.delete('movie_posters');
|
||||
await txn.delete('movies');
|
||||
await txn.delete('books');
|
||||
await txn.delete('notes');
|
||||
|
||||
// 导入影视数据(更新图片路径)
|
||||
if (data.containsKey('movies')) {
|
||||
final movies = data['movies'] as List<dynamic>;
|
||||
for (final movie in movies) {
|
||||
final movieMap = _convertToDbMap(movie);
|
||||
final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap);
|
||||
await txn.insert('movies', updatedMap);
|
||||
}
|
||||
}
|
||||
|
||||
// 导入书籍数据(更新图片路径)
|
||||
if (data.containsKey('books')) {
|
||||
final books = data['books'] as List<dynamic>;
|
||||
for (final book in books) {
|
||||
final bookMap = _convertToDbMap(book);
|
||||
final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap);
|
||||
await txn.insert('books', updatedMap);
|
||||
}
|
||||
}
|
||||
|
||||
// 导入笔记数据
|
||||
if (data.containsKey('notes')) {
|
||||
final notes = data['notes'] as List<dynamic>;
|
||||
for (final note in notes) {
|
||||
await txn.insert('notes', _convertToDbMap(note));
|
||||
}
|
||||
}
|
||||
|
||||
// 导入影评数据
|
||||
if (data.containsKey('movie_reviews')) {
|
||||
final reviews = data['movie_reviews'] as List<dynamic>;
|
||||
for (final review in reviews) {
|
||||
await txn.insert('movie_reviews', _convertToDbMap(review));
|
||||
}
|
||||
}
|
||||
|
||||
// 导入海报墙数据(更新图片路径)
|
||||
if (data.containsKey('movie_posters')) {
|
||||
final posters = data['movie_posters'] as List<dynamic>;
|
||||
for (final poster in posters) {
|
||||
final posterMap = _convertToDbMap(poster);
|
||||
final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap);
|
||||
await txn.insert('movie_posters', updatedMap);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 统计导入数量
|
||||
final stats = <String, int>{};
|
||||
if (data.containsKey('movies')) {
|
||||
stats['影视'] = (data['movies'] as List).length;
|
||||
}
|
||||
if (data.containsKey('books')) {
|
||||
stats['书籍'] = (data['books'] as List).length;
|
||||
}
|
||||
if (data.containsKey('notes')) {
|
||||
stats['笔记'] = (data['notes'] as List).length;
|
||||
}
|
||||
if (data.containsKey('movie_reviews')) {
|
||||
stats['影评'] = (data['movie_reviews'] as List).length;
|
||||
}
|
||||
if (data.containsKey('movie_posters')) {
|
||||
stats['海报'] = (data['movie_posters'] as List).length;
|
||||
}
|
||||
if (imageCount > 0) {
|
||||
stats['图片'] = imageCount;
|
||||
}
|
||||
|
||||
return ImportResult.success(stats);
|
||||
} catch (e) {
|
||||
return ImportResult.error('导入失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 将动态类型转换为数据库可用的 Map
|
||||
Map<String, dynamic> _convertToDbMap(dynamic item) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
return item.map((key, value) {
|
||||
// 处理布尔值
|
||||
if (value is bool) {
|
||||
return MapEntry(key, value ? 1 : 0);
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/// 更新图片路径为新的路径
|
||||
Map<String, dynamic> _updateImagePath(
|
||||
Map<String, dynamic> item,
|
||||
String pathField,
|
||||
Map<String, String> imagePathMap,
|
||||
) {
|
||||
final newItem = Map<String, dynamic>.from(item);
|
||||
final oldPath = item[pathField] as String?;
|
||||
|
||||
if (oldPath != null && oldPath.isNotEmpty) {
|
||||
final fileName = path.basename(oldPath);
|
||||
// 如果图片在映射中,更新路径
|
||||
if (imagePathMap.containsKey(fileName)) {
|
||||
newItem[pathField] = imagePathMap[fileName];
|
||||
}
|
||||
}
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
/// 格式化日期时间用于文件名
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.year}${_pad(dateTime.month)}${_pad(dateTime.day)}_${_pad(dateTime.hour)}${_pad(dateTime.minute)}${_pad(dateTime.second)}';
|
||||
}
|
||||
|
||||
String _pad(int number) {
|
||||
return number.toString().padLeft(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
/// 导出结果
|
||||
class ExportResult {
|
||||
final bool success;
|
||||
final bool cancelled;
|
||||
final String? errorMessage;
|
||||
final String? filePath;
|
||||
final int movieCount;
|
||||
final int bookCount;
|
||||
final int noteCount;
|
||||
final int imageCount;
|
||||
|
||||
ExportResult._({
|
||||
required this.success,
|
||||
this.cancelled = false,
|
||||
this.errorMessage,
|
||||
this.filePath,
|
||||
this.movieCount = 0,
|
||||
this.bookCount = 0,
|
||||
this.noteCount = 0,
|
||||
this.imageCount = 0,
|
||||
});
|
||||
|
||||
factory ExportResult.success({
|
||||
required String filePath,
|
||||
required int movieCount,
|
||||
required int bookCount,
|
||||
required int noteCount,
|
||||
required int imageCount,
|
||||
}) {
|
||||
return ExportResult._(
|
||||
success: true,
|
||||
filePath: filePath,
|
||||
movieCount: movieCount,
|
||||
bookCount: bookCount,
|
||||
noteCount: noteCount,
|
||||
imageCount: imageCount,
|
||||
);
|
||||
}
|
||||
|
||||
factory ExportResult.cancelled() {
|
||||
return ExportResult._(success: false, cancelled: true);
|
||||
}
|
||||
|
||||
factory ExportResult.error(String message) {
|
||||
return ExportResult._(success: false, errorMessage: message);
|
||||
}
|
||||
}
|
||||
|
||||
/// 导入结果
|
||||
class ImportResult {
|
||||
final bool success;
|
||||
final bool cancelled;
|
||||
final String? errorMessage;
|
||||
final Map<String, int>? stats;
|
||||
|
||||
ImportResult._({
|
||||
required this.success,
|
||||
this.cancelled = false,
|
||||
this.errorMessage,
|
||||
this.stats,
|
||||
});
|
||||
|
||||
factory ImportResult.success(Map<String, int> stats) {
|
||||
return ImportResult._(success: true, stats: stats);
|
||||
}
|
||||
|
||||
factory ImportResult.cancelled() {
|
||||
return ImportResult._(success: false, cancelled: true);
|
||||
}
|
||||
|
||||
factory ImportResult.error(String message) {
|
||||
return ImportResult._(success: false, errorMessage: message);
|
||||
}
|
||||
|
||||
/// 获取统计信息文本
|
||||
String get statsText {
|
||||
if (stats == null || stats!.isEmpty) {
|
||||
return '没有导入任何数据';
|
||||
}
|
||||
return stats!.entries.map((e) => '${e.key}: ${e.value}').join(',');
|
||||
}
|
||||
}
|
||||
@@ -124,4 +124,40 @@ class BookDao {
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的书籍
|
||||
Future<List<Book>> getDeletedBooks() async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [1],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
|
||||
// 恢复已删除的书籍
|
||||
Future<int> restoreBook(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'books',
|
||||
{'is_deleted': 0, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
// 彻底删除书籍
|
||||
Future<int> permanentDeleteBook(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'books',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
109
lib/utils/book_excerpt_dao.dart
Normal file
109
lib/utils/book_excerpt_dao.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'database_helper.dart';
|
||||
|
||||
/// 书籍摘抄数据访问对象
|
||||
class BookExcerptDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
/// 获取书籍的所有摘抄
|
||||
Future<List<BookExcerpt>> getExcerptsByBookId(String bookId) async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_excerpts',
|
||||
where: 'book_id = ? AND is_deleted = 0',
|
||||
whereArgs: [bookId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取摘抄
|
||||
Future<BookExcerpt?> getExcerptById(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_excerpts',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isNotEmpty) {
|
||||
return BookExcerpt.fromJson(maps.first);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 插入摘抄
|
||||
Future<String> insertExcerpt(BookExcerpt excerpt) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
'book_excerpts',
|
||||
excerpt.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return excerpt.id;
|
||||
}
|
||||
|
||||
/// 更新摘抄
|
||||
Future<void> updateExcerpt(BookExcerpt excerpt) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
excerpt.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [excerpt.id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 删除摘抄(软删除)
|
||||
Future<void> deleteExcerpt(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
{'is_deleted': 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 彻底删除摘抄
|
||||
Future<void> permanentDeleteExcerpt(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'book_excerpts',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取摘抄数量
|
||||
Future<int> getExcerptCount(String bookId) async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM book_excerpts WHERE book_id = ? AND is_deleted = 0',
|
||||
[bookId],
|
||||
);
|
||||
return result.first['count'] as int? ?? 0;
|
||||
}
|
||||
|
||||
/// 获取所有已删除的摘抄
|
||||
Future<List<BookExcerpt>> getDeletedExcerpts() async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_excerpts',
|
||||
where: 'is_deleted = 1',
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
|
||||
}
|
||||
|
||||
/// 恢复已删除的摘抄
|
||||
Future<void> restoreExcerpt(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
{'is_deleted': 0},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/utils/book_review_dao.dart
Normal file
109
lib/utils/book_review_dao.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'database_helper.dart';
|
||||
|
||||
/// 书评数据访问对象
|
||||
class BookReviewDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
/// 获取书籍的所有书评
|
||||
Future<List<BookReview>> getReviewsByBookId(String bookId) async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_reviews',
|
||||
where: 'book_id = ? AND is_deleted = 0',
|
||||
whereArgs: [bookId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookReview.fromJson(map)).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取书评
|
||||
Future<BookReview?> getReviewById(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_reviews',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isNotEmpty) {
|
||||
return BookReview.fromJson(maps.first);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 插入书评
|
||||
Future<String> insertReview(BookReview review) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
'book_reviews',
|
||||
review.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return review.id;
|
||||
}
|
||||
|
||||
/// 更新书评
|
||||
Future<void> updateReview(BookReview review) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
review.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [review.id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 删除书评(软删除)
|
||||
Future<void> deleteReview(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
{'is_deleted': 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 彻底删除书评
|
||||
Future<void> permanentDeleteReview(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'book_reviews',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取书评数量
|
||||
Future<int> getReviewCount(String bookId) async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM book_reviews WHERE book_id = ? AND is_deleted = 0',
|
||||
[bookId],
|
||||
);
|
||||
return result.first['count'] as int? ?? 0;
|
||||
}
|
||||
|
||||
/// 获取所有已删除的书评
|
||||
Future<List<BookReview>> getDeletedReviews() async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_reviews',
|
||||
where: 'is_deleted = 1',
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookReview.fromJson(map)).toList();
|
||||
}
|
||||
|
||||
/// 恢复已删除的书评
|
||||
Future<void> restoreReview(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
{'is_deleted': 0},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class DatabaseHelper {
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 5,
|
||||
version: 8,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
@@ -45,6 +45,31 @@ class DatabaseHelper {
|
||||
await _createMovieReviewsTable(db);
|
||||
await _createMoviePostersTable(db);
|
||||
}
|
||||
if (oldVersion < 6) {
|
||||
// 为笔记表添加软删除字段
|
||||
await _upgradeNotesTableV6(db);
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
// 创建书评表和摘抄表
|
||||
await _createBookReviewsTable(db);
|
||||
await _createBookExcerptsTable(db);
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
// 确保书评表和摘抄表存在(兼容之前版本未成功创建的情况)
|
||||
await _createBookReviewsTable(db);
|
||||
await _createBookExcerptsTable(db);
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级notes表到V6(添加软删除字段)
|
||||
Future<void> _upgradeNotesTableV6(Database db) async {
|
||||
// 检查是否存在 is_deleted 列
|
||||
final columns = await db.rawQuery('PRAGMA table_info(notes)');
|
||||
final hasIsDeleted = columns.any((col) => col['name'] == 'is_deleted');
|
||||
|
||||
if (!hasIsDeleted) {
|
||||
await db.execute('ALTER TABLE notes ADD COLUMN is_deleted INTEGER DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建影评表
|
||||
@@ -64,7 +89,7 @@ class DatabaseHelper {
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
|
||||
/// 创建影视海报墙表
|
||||
Future<void> _createMoviePostersTable(Database db) async {
|
||||
await db.execute('''
|
||||
@@ -78,6 +103,41 @@ class DatabaseHelper {
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
/// 创建书评表
|
||||
Future<void> _createBookReviewsTable(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS book_reviews (
|
||||
id TEXT PRIMARY KEY,
|
||||
book_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
reviewer TEXT,
|
||||
source TEXT,
|
||||
review_type INTEGER DEFAULT 1,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (book_id) REFERENCES books (id)
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
/// 创建书籍摘抄表
|
||||
Future<void> _createBookExcerptsTable(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS book_excerpts (
|
||||
id TEXT PRIMARY KEY,
|
||||
book_id TEXT NOT NULL,
|
||||
chapter TEXT,
|
||||
content TEXT NOT NULL,
|
||||
comment TEXT,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (book_id) REFERENCES books (id)
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
/// 升级notes表到V4
|
||||
Future<void> _upgradeNotesTableV4(Database db) async {
|
||||
@@ -285,7 +345,8 @@ class DatabaseHelper {
|
||||
content_type TEXT DEFAULT 'markdown',
|
||||
tags TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
@@ -316,6 +377,37 @@ class DatabaseHelper {
|
||||
FOREIGN KEY (movie_id) REFERENCES movies (id)
|
||||
)
|
||||
''');
|
||||
|
||||
// 书评表
|
||||
await db.execute('''
|
||||
CREATE TABLE book_reviews (
|
||||
id TEXT PRIMARY KEY,
|
||||
book_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
reviewer TEXT,
|
||||
source TEXT,
|
||||
review_type INTEGER DEFAULT 1,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (book_id) REFERENCES books (id)
|
||||
)
|
||||
''');
|
||||
|
||||
// 书籍摘抄表
|
||||
await db.execute('''
|
||||
CREATE TABLE book_excerpts (
|
||||
id TEXT PRIMARY KEY,
|
||||
book_id TEXT NOT NULL,
|
||||
chapter TEXT,
|
||||
content TEXT NOT NULL,
|
||||
comment TEXT,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (book_id) REFERENCES books (id)
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
// 关闭数据库
|
||||
|
||||
@@ -177,4 +177,40 @@ class MovieDao {
|
||||
}
|
||||
return genres.toList()..sort();
|
||||
}
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的影视
|
||||
Future<List<Movie>> getDeletedMovies() async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [1],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
}
|
||||
|
||||
// 恢复已删除的影视
|
||||
Future<int> restoreMovie(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movies',
|
||||
{'is_deleted': 0, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
// 彻底删除影视
|
||||
Future<int> permanentDeleteMovie(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'movies',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import 'database_helper.dart';
|
||||
class NoteDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
// 获取所有笔记
|
||||
// 获取所有未删除的笔记
|
||||
Future<List<Note>> getAllNotes() async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
|
||||
@@ -47,8 +49,45 @@ class NoteDao {
|
||||
);
|
||||
}
|
||||
|
||||
// 删除笔记
|
||||
// 软删除笔记
|
||||
Future<int> deleteNote(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的笔记
|
||||
Future<List<Note>> getDeletedNotes() async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [1],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
}
|
||||
|
||||
// 恢复已删除的笔记
|
||||
Future<int> restoreNote(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
{'is_deleted': 0, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
// 彻底删除笔记
|
||||
Future<int> permanentDeleteNote(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'notes',
|
||||
@@ -62,8 +101,8 @@ class NoteDao {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'content LIKE ? OR tags LIKE ?',
|
||||
whereArgs: ['%$query%', '%$query%'],
|
||||
where: '(content LIKE ? OR tags LIKE ?) AND is_deleted = ?',
|
||||
whereArgs: ['%$query%', '%$query%', 0],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
|
||||
@@ -75,8 +114,8 @@ class NoteDao {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'tags LIKE ?',
|
||||
whereArgs: ['%$tag%'],
|
||||
where: 'tags LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$tag%', 0],
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
|
||||
|
||||
49
lib/utils/toast_util.dart
Normal file
49
lib/utils/toast_util.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Toast 工具类
|
||||
class ToastUtil {
|
||||
static OverlayEntry? _currentToast;
|
||||
|
||||
/// 显示 Toast
|
||||
static void show(BuildContext context, String message) {
|
||||
// 移除之前的 Toast
|
||||
_currentToast?.remove();
|
||||
_currentToast = null;
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
_currentToast = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A).withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(_currentToast!);
|
||||
|
||||
// 2秒后自动消失
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
_currentToast?.remove();
|
||||
_currentToast = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user