generated from dellevin/template
优化项目结构
This commit is contained in:
186
lib/data/book/book_dao.dart
Normal file
186
lib/data/book/book_dao.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 书籍数据访问对象
|
||||
class BookDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[BookDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有未删除的书籍记录
|
||||
Future<List<Book>> getAllBooks() => _wrap('getAllBooks', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 分页查询书籍记录
|
||||
Future<List<Book>> getBooksPaged({String? status, int limit = 20, int offset = 0, int sortMode = 0}) => _wrap('getBooksPaged', () async {
|
||||
final db = await _dbHelper.database;
|
||||
String where = 'is_deleted = 0';
|
||||
List<dynamic> whereArgs = [];
|
||||
if (status != null && status.isNotEmpty) {
|
||||
where += ' AND status = ?';
|
||||
whereArgs.add(status);
|
||||
}
|
||||
final maps = await db.query('books', where: where, whereArgs: whereArgs,
|
||||
orderBy: _buildBookOrderBy(sortMode), limit: limit, offset: offset);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
static String _buildBookOrderBy(int sortMode) {
|
||||
switch (sortMode) {
|
||||
case 1: return 'created_at DESC';
|
||||
case 2: return 'rating DESC NULLS LAST, updated_at DESC';
|
||||
default: return 'updated_at DESC';
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态筛选书籍记录
|
||||
Future<List<Book>> getBooksByStatus(String status) => _wrap('getBooksByStatus', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'status = ? AND is_deleted = ?',
|
||||
whereArgs: [status, 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 根据ID获取书籍
|
||||
Future<Book?> getBookById(String id) => _wrap('getBookById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'id = ? AND is_deleted = ?',
|
||||
whereArgs: [id, 0],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return Book.fromJson(maps.first);
|
||||
});
|
||||
|
||||
// 添加书籍记录
|
||||
Future<int> insertBook(Book book) => _wrap('insertBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('books', book.toJson());
|
||||
});
|
||||
|
||||
// 更新书籍记录
|
||||
Future<int> updateBook(Book book) => _wrap('updateBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'books',
|
||||
book.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [book.id],
|
||||
);
|
||||
});
|
||||
|
||||
// 仅更新封面偏移量(不触发全量刷新)
|
||||
Future<void> updateCoverOffset(String bookId, double offset) => _wrap('updateCoverOffset', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update('books', {'cover_offset': offset}, where: 'id = ?', whereArgs: [bookId]);
|
||||
});
|
||||
|
||||
// 软删除书籍记录(移入回收站)
|
||||
Future<int> deleteBook(String id) => _wrap('deleteBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'books',
|
||||
{
|
||||
'is_deleted': 1,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
// 搜索书籍(标题、别名)
|
||||
Future<List<Book>> searchBooks(String query) => _wrap('searchBooks', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: '(title LIKE ? OR alternate_titles LIKE ?) AND is_deleted = ?',
|
||||
whereArgs: ['%$query%', '%$query%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 根据作者筛选
|
||||
Future<List<Book>> getBooksByAuthor(String author) => _wrap('getBooksByAuthor', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'authors LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$author%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 根据类型筛选
|
||||
Future<List<Book>> getBooksByGenre(String genre) => _wrap('getBooksByGenre', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'genres LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$genre%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的书籍
|
||||
Future<List<Book>> getDeletedBooks() => _wrap('getDeletedBooks', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [1],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 恢复已删除的书籍
|
||||
Future<int> restoreBook(String id) => _wrap('restoreBook', () 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) => _wrap('permanentDeleteBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
// 清理子记录,防止孤儿数据
|
||||
await db.delete('book_reviews', where: 'book_id = ?', whereArgs: [id]);
|
||||
await db.delete('book_excerpts', where: 'book_id = ?', whereArgs: [id]);
|
||||
return await db.delete(
|
||||
'books',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
}
|
||||
117
lib/data/book/book_excerpt_dao.dart
Normal file
117
lib/data/book/book_excerpt_dao.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 书籍摘抄数据访问对象
|
||||
class BookExcerptDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[BookExcerptDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取书籍的所有摘抄
|
||||
Future<List<BookExcerpt>> getExcerptsByBookId(String bookId) => _wrap('getExcerptsByBookId', () 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) => _wrap('getExcerptById', () 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) => _wrap('insertExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
'book_excerpts',
|
||||
excerpt.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return excerpt.id;
|
||||
});
|
||||
|
||||
/// 更新摘抄
|
||||
Future<void> updateExcerpt(BookExcerpt excerpt) => _wrap('updateExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
excerpt.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [excerpt.id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 删除摘抄(软删除)
|
||||
Future<void> deleteExcerpt(String id) => _wrap('deleteExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toUtc().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 彻底删除摘抄
|
||||
Future<void> permanentDeleteExcerpt(String id) => _wrap('permanentDeleteExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'book_excerpts',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 获取摘抄数量
|
||||
Future<int> getExcerptCount(String bookId) => _wrap('getExcerptCount', () 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() => _wrap('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) => _wrap('restoreExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
{'is_deleted': 0},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
}
|
||||
117
lib/data/book/book_review_dao.dart
Normal file
117
lib/data/book/book_review_dao.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 书评数据访问对象
|
||||
class BookReviewDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[BookReviewDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取书籍的所有书评
|
||||
Future<List<BookReview>> getReviewsByBookId(String bookId) => _wrap('getReviewsByBookId', () 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) => _wrap('getReviewById', () 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) => _wrap('insertReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
'book_reviews',
|
||||
review.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return review.id;
|
||||
});
|
||||
|
||||
/// 更新书评
|
||||
Future<void> updateReview(BookReview review) => _wrap('updateReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
review.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [review.id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 删除书评(软删除)
|
||||
Future<void> deleteReview(String id) => _wrap('deleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toUtc().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 彻底删除书评
|
||||
Future<void> permanentDeleteReview(String id) => _wrap('permanentDeleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'book_reviews',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 获取书评数量
|
||||
Future<int> getReviewCount(String bookId) => _wrap('getReviewCount', () 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() => _wrap('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) => _wrap('restoreReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
{'is_deleted': 0},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
}
|
||||
844
lib/data/database_helper.dart
Normal file
844
lib/data/database_helper.dart
Normal file
@@ -0,0 +1,844 @@
|
||||
import 'dart:async';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 数据库帮助类 - 管理数据库的创建和版本控制
|
||||
class DatabaseHelper {
|
||||
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||
static Database? _database;
|
||||
static Completer<void>? _reopenCompleter;
|
||||
static Completer<Database>? _initCompleter;
|
||||
|
||||
DatabaseHelper._init();
|
||||
|
||||
/// 数据库文件路径
|
||||
Future<String?> get databasePath async {
|
||||
final path = await getDatabasesPath();
|
||||
return join(path, 'mooknote.db');
|
||||
}
|
||||
|
||||
/// 重新打开数据库(用于 WebDAV 同步后)
|
||||
Future<void> reopenDatabase() async {
|
||||
// 如果已有重开在进行,等待它完成即可
|
||||
if (_reopenCompleter != null) {
|
||||
return _reopenCompleter!.future;
|
||||
}
|
||||
_reopenCompleter = Completer<void>();
|
||||
try {
|
||||
if (_database != null) {
|
||||
await _database!.close();
|
||||
_database = null;
|
||||
}
|
||||
_database = await _initDB('mooknote.db');
|
||||
_reopenCompleter!.complete();
|
||||
} catch (e) {
|
||||
_reopenCompleter!.completeError(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_reopenCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Database> get database async {
|
||||
// 如果正在重开,等待完成
|
||||
if (_reopenCompleter != null) {
|
||||
await _reopenCompleter!.future;
|
||||
}
|
||||
if (_database != null) return _database!;
|
||||
|
||||
// 如果已有初始化在进行,等待它完成
|
||||
if (_initCompleter != null) {
|
||||
return _initCompleter!.future;
|
||||
}
|
||||
|
||||
_initCompleter = Completer<Database>();
|
||||
try {
|
||||
_database = await _initDB('mooknote.db');
|
||||
_initCompleter!.complete(_database!);
|
||||
return _database!;
|
||||
} catch (e) {
|
||||
_initCompleter!.completeError(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_initCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Database> _initDB(String filePath) async {
|
||||
final dbPath = await getDatabasesPath();
|
||||
final path = join(dbPath, filePath);
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 30,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
}
|
||||
|
||||
/// 数据库升级
|
||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
// 升级movies表结构
|
||||
await _upgradeMoviesTableV2(db);
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
// 升级books表结构
|
||||
await _upgradeBooksTableV3(db);
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
// 升级notes表结构
|
||||
await _upgradeNotesTableV4(db);
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
// 创建影评表和海报墙表
|
||||
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);
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
// 为笔记表添加图片字段
|
||||
await _upgradeNotesTableV9(db);
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
// 为影视表添加观看日期字段
|
||||
await _upgradeMoviesTableV10(db);
|
||||
}
|
||||
if (oldVersion < 11) {
|
||||
// 为书籍表添加ISBN和出版时间字段
|
||||
await _upgradeBooksTableV11(db);
|
||||
}
|
||||
if (oldVersion < 12) {
|
||||
// 确保notes表有title列
|
||||
await _upgradeNotesTableV12(db);
|
||||
}
|
||||
if (oldVersion < 13) {
|
||||
await _upgradeToV13(db);
|
||||
}
|
||||
if (oldVersion < 14) {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS reader_books (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT DEFAULT '',
|
||||
cover_path TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
file_extension TEXT NOT NULL DEFAULT 'epub',
|
||||
last_read_cfi TEXT DEFAULT '',
|
||||
reading_percentage REAL DEFAULT 0.0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
}
|
||||
if (oldVersion < 15) {
|
||||
// 安全添加 cover_offset 列(防止列已存在时报错)
|
||||
final movieCols = await db.rawQuery('PRAGMA table_info(movies)');
|
||||
if (!movieCols.any((col) => col['name'] == 'cover_offset')) {
|
||||
await db.execute('ALTER TABLE movies ADD COLUMN cover_offset REAL DEFAULT 0');
|
||||
}
|
||||
final bookCols = await db.rawQuery('PRAGMA table_info(books)');
|
||||
if (!bookCols.any((col) => col['name'] == 'cover_offset')) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
// v16: 确保 cover_offset 列存在(v15 的数据库可能缺少此列)
|
||||
if (oldVersion < 16) {
|
||||
final movieCols = await db.rawQuery('PRAGMA table_info(movies)');
|
||||
if (!movieCols.any((col) => col['name'] == 'cover_offset')) {
|
||||
await db.execute('ALTER TABLE movies ADD COLUMN cover_offset REAL DEFAULT 0');
|
||||
}
|
||||
final bookCols = await db.rawQuery('PRAGMA table_info(books)');
|
||||
if (!bookCols.any((col) => col['name'] == 'cover_offset')) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN cover_offset REAL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
if (oldVersion < 17) {
|
||||
await db.execute('ALTER TABLE tags ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
if (oldVersion < 18) {
|
||||
// note_plus table creation removed (feature dropped in v28)
|
||||
}
|
||||
if (oldVersion < 19) {
|
||||
// note_plus table creation removed (feature dropped in v28)
|
||||
}
|
||||
if (oldVersion < 20) {
|
||||
// 确保 note_plus 表有 parent_id 列(从旧版 folder 迁移)
|
||||
try {
|
||||
final cols = await db.rawQuery('PRAGMA table_info(note_plus)');
|
||||
if (!cols.any((col) => col['name'] == 'parent_id')) {
|
||||
if (cols.any((col) => col['name'] == 'folder')) {
|
||||
await db.execute("ALTER TABLE note_plus RENAME COLUMN folder TO parent_id");
|
||||
} else {
|
||||
await db.execute("ALTER TABLE note_plus ADD COLUMN parent_id TEXT DEFAULT ''");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Migration v20 failed: $e');
|
||||
}
|
||||
}
|
||||
if (oldVersion < 21) {
|
||||
try {
|
||||
final cols = await db.rawQuery('PRAGMA table_info(note_plus)');
|
||||
if (!cols.any((col) => col['name'] == 'sort_index')) {
|
||||
await db.execute("ALTER TABLE note_plus ADD COLUMN sort_index INTEGER DEFAULT 0");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Migration v21 failed: $e');
|
||||
}
|
||||
}
|
||||
if (oldVersion < 22) {
|
||||
// 为笔记表添加置顶字段
|
||||
await _upgradeNotesTableV22(db);
|
||||
}
|
||||
if (oldVersion < 23) {
|
||||
// 创建书籍批注表(高亮、下划线、书签)
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS book_annotations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
cfi TEXT NOT NULL DEFAULT '',
|
||||
chapter TEXT DEFAULT '',
|
||||
type TEXT NOT NULL DEFAULT 'highlight',
|
||||
color TEXT NOT NULL DEFAULT 'FFEB3B',
|
||||
reader_note TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS idx_book_annotations_book_id ON book_annotations(book_id)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS idx_book_annotations_type ON book_annotations(book_id, type)',
|
||||
);
|
||||
}
|
||||
if (oldVersion < 24) {
|
||||
// reader_books 添加 book_id 列(关联 books 表)
|
||||
final cols = await db.rawQuery('PRAGMA table_info(reader_books)');
|
||||
if (!cols.any((col) => col['name'] == 'book_id')) {
|
||||
await db.execute("ALTER TABLE reader_books ADD COLUMN book_id TEXT DEFAULT ''");
|
||||
}
|
||||
}
|
||||
if (oldVersion < 25) {
|
||||
// movies 添加 category 列(影视分类)
|
||||
final cols = await db.rawQuery('PRAGMA table_info(movies)');
|
||||
if (!cols.any((col) => col['name'] == 'category')) {
|
||||
await db.execute("ALTER TABLE movies ADD COLUMN category TEXT NOT NULL DEFAULT 'movie'");
|
||||
}
|
||||
}
|
||||
if (oldVersion < 26) {
|
||||
await _upgradeBooksTableV26(db);
|
||||
}
|
||||
if (oldVersion < 27) {
|
||||
await _upgradeBooksTableV27(db);
|
||||
}
|
||||
if (oldVersion < 28) {
|
||||
// 移除 Note Plus 功能,删除 note_plus 表
|
||||
await db.execute('DROP TABLE IF EXISTS note_plus');
|
||||
}
|
||||
if (oldVersion < 29) {
|
||||
// 为书籍表添加译者字段
|
||||
final bookCols = await db.rawQuery('PRAGMA table_info(books)');
|
||||
if (!bookCols.any((col) => col['name'] == 'translators')) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN translators TEXT');
|
||||
}
|
||||
}
|
||||
if (oldVersion < 30) {
|
||||
// reader_books 添加简介、出版社、ISBN、多作者字段
|
||||
final cols = await db.rawQuery('PRAGMA table_info(reader_books)');
|
||||
if (!cols.any((col) => col['name'] == 'summary')) {
|
||||
await db.execute("ALTER TABLE reader_books ADD COLUMN summary TEXT DEFAULT ''");
|
||||
}
|
||||
if (!cols.any((col) => col['name'] == 'publisher')) {
|
||||
await db.execute("ALTER TABLE reader_books ADD COLUMN publisher TEXT DEFAULT ''");
|
||||
}
|
||||
if (!cols.any((col) => col['name'] == 'isbn')) {
|
||||
await db.execute("ALTER TABLE reader_books ADD COLUMN isbn TEXT DEFAULT ''");
|
||||
}
|
||||
if (!cols.any((col) => col['name'] == 'authors')) {
|
||||
await db.execute("ALTER TABLE reader_books ADD COLUMN authors TEXT DEFAULT ''");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级books表到V27(添加阅读始末日期字段)
|
||||
Future<void> _upgradeBooksTableV27(Database db) async {
|
||||
final columns = await db.rawQuery('PRAGMA table_info(books)');
|
||||
final hasStartDate = columns.any((col) => col['name'] == 'start_date');
|
||||
final hasFinishDate = columns.any((col) => col['name'] == 'finish_date');
|
||||
|
||||
if (!hasStartDate) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN start_date TEXT');
|
||||
}
|
||||
if (!hasFinishDate) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN finish_date TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级books表到V26(添加阅读始末日期字段)
|
||||
Future<void> _upgradeBooksTableV26(Database db) async {
|
||||
final columns = await db.rawQuery('PRAGMA table_info(books)');
|
||||
final hasStartDate = columns.any((col) => col['name'] == 'start_date');
|
||||
final hasFinishDate = columns.any((col) => col['name'] == 'finish_date');
|
||||
|
||||
if (!hasStartDate) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN start_date TEXT');
|
||||
}
|
||||
if (!hasFinishDate) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN finish_date TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级books表到V11(添加ISBN和出版时间字段)
|
||||
Future<void> _upgradeBooksTableV11(Database db) async {
|
||||
// 检查是否存在 isbn 列
|
||||
final columns = await db.rawQuery('PRAGMA table_info(books)');
|
||||
final hasIsbn = columns.any((col) => col['name'] == 'isbn');
|
||||
final hasPublishDate = columns.any((col) => col['name'] == 'publish_date');
|
||||
|
||||
if (!hasIsbn) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN isbn TEXT');
|
||||
}
|
||||
if (!hasPublishDate) {
|
||||
await db.execute('ALTER TABLE books ADD COLUMN publish_date TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级movies表到V10(添加观看日期字段)
|
||||
Future<void> _upgradeMoviesTableV10(Database db) async {
|
||||
// 检查是否存在 watch_date 列
|
||||
final columns = await db.rawQuery('PRAGMA table_info(movies)');
|
||||
final hasWatchDate = columns.any((col) => col['name'] == 'watch_date');
|
||||
|
||||
if (!hasWatchDate) {
|
||||
await db.execute('ALTER TABLE movies ADD COLUMN watch_date TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级notes表到V12(确保title列存在)
|
||||
Future<void> _upgradeNotesTableV12(Database db) async {
|
||||
// 检查是否存在 title 列
|
||||
final columns = await db.rawQuery('PRAGMA table_info(notes)');
|
||||
final hasTitle = columns.any((col) => col['name'] == 'title');
|
||||
|
||||
if (!hasTitle) {
|
||||
await db.execute('ALTER TABLE notes ADD COLUMN title TEXT DEFAULT \'\'');
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级notes表到V22(添加置顶字段)
|
||||
Future<void> _upgradeNotesTableV22(Database db) async {
|
||||
final columns = await db.rawQuery('PRAGMA table_info(notes)');
|
||||
final hasIsPinned = columns.any((col) => col['name'] == 'is_pinned');
|
||||
if (!hasIsPinned) {
|
||||
await db.execute('ALTER TABLE notes ADD COLUMN is_pinned INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级到V13:创建标签表并回填已有数据
|
||||
Future<void> _upgradeToV13(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
is_hidden INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(name, type)
|
||||
)
|
||||
''');
|
||||
await _backfillTags(db);
|
||||
}
|
||||
|
||||
Future<void> _backfillTags(Database db) async {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
int counter = 0;
|
||||
|
||||
Future<void> insertTag(String name, String type) async {
|
||||
try {
|
||||
await db.insert('tags', {
|
||||
'id': 'tag_${DateTime.now().millisecondsSinceEpoch}_${counter++}',
|
||||
'name': name,
|
||||
'type': type,
|
||||
'created_at': now,
|
||||
});
|
||||
} catch (_) {
|
||||
// 忽略 UNIQUE 约束冲突
|
||||
}
|
||||
}
|
||||
|
||||
// 回填影视类型
|
||||
final movies = await db.query('movies',
|
||||
where: 'genres IS NOT NULL AND genres != ?', whereArgs: ['[]']);
|
||||
for (final row in movies) {
|
||||
for (final genre in parseStringListGeneric(row['genres'])) {
|
||||
await insertTag(genre, 'movie_genre');
|
||||
}
|
||||
}
|
||||
|
||||
// 回填书籍类型
|
||||
final books = await db.query('books',
|
||||
where: 'genres IS NOT NULL AND genres != ?', whereArgs: ['[]']);
|
||||
for (final row in books) {
|
||||
for (final genre in parseStringListGeneric(row['genres'])) {
|
||||
await insertTag(genre, 'book_genre');
|
||||
}
|
||||
}
|
||||
|
||||
// 回填笔记标签
|
||||
final notes = await db.query('notes',
|
||||
where: 'tags IS NOT NULL AND tags != ? AND tags != ?',
|
||||
whereArgs: ['[]', '']);
|
||||
for (final row in notes) {
|
||||
for (final tag in parseStringListGeneric(row['tags'])) {
|
||||
await insertTag(tag, 'note_tag');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级notes表到V9(添加图片字段)
|
||||
Future<void> _upgradeNotesTableV9(Database db) async {
|
||||
// 检查是否存在 images 列
|
||||
final columns = await db.rawQuery('PRAGMA table_info(notes)');
|
||||
final hasImages = columns.any((col) => col['name'] == 'images');
|
||||
|
||||
if (!hasImages) {
|
||||
await db.execute('ALTER TABLE notes ADD COLUMN images TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级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');
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建影评表
|
||||
Future<void> _createMovieReviewsTable(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS movie_reviews (
|
||||
id TEXT PRIMARY KEY,
|
||||
movie_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 (movie_id) REFERENCES movies (id)
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
/// 创建影视海报墙表
|
||||
Future<void> _createMoviePostersTable(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS movie_posters (
|
||||
id TEXT PRIMARY KEY,
|
||||
movie_id TEXT NOT NULL,
|
||||
poster_path TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (movie_id) REFERENCES movies (id)
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
/// 创建书评表
|
||||
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 {
|
||||
// 备份旧数据
|
||||
final oldData = await db.query('notes');
|
||||
|
||||
// 删除旧表
|
||||
await db.execute('DROP TABLE IF EXISTS notes');
|
||||
|
||||
// 创建新表
|
||||
await db.execute('''
|
||||
CREATE TABLE notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
content_type TEXT DEFAULT 'markdown',
|
||||
tags TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据(将title字段恢复)
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
final title = row['title']?.toString() ?? '';
|
||||
final content = row['content']?.toString() ?? '';
|
||||
|
||||
await db.insert('notes', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': title,
|
||||
'content': content,
|
||||
'content_type': 'markdown',
|
||||
'tags': row['tags'] ?? '',
|
||||
'created_at': row['created_at']?.toString() ?? now,
|
||||
'updated_at': row['updated_at']?.toString() ?? now,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移笔记记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级books表到V3
|
||||
Future<void> _upgradeBooksTableV3(Database db) async {
|
||||
// 备份旧数据
|
||||
final oldData = await db.query('books');
|
||||
|
||||
// 删除旧表
|
||||
await db.execute('DROP TABLE IF EXISTS books');
|
||||
|
||||
// 创建新表
|
||||
await db.execute('''
|
||||
CREATE TABLE books (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
cover_path TEXT,
|
||||
authors TEXT,
|
||||
alternate_titles TEXT,
|
||||
publisher TEXT,
|
||||
genres TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
await db.insert('books', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': row['title']?.toString() ?? '',
|
||||
'cover_path': row['cover'],
|
||||
'authors': row['author'] != null ? '["${row['author']}"]' : '[]',
|
||||
'alternate_titles': '[]',
|
||||
'publisher': null,
|
||||
'genres': '[]',
|
||||
'summary': row['note'],
|
||||
'rating': row['rating'],
|
||||
'status': row['status'] ?? 'want_to_read',
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
'is_deleted': 0,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移书籍记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级movies表到V2
|
||||
Future<void> _upgradeMoviesTableV2(Database db) async {
|
||||
// 备份旧数据
|
||||
final oldData = await db.query('movies');
|
||||
|
||||
// 删除旧表
|
||||
await db.execute('DROP TABLE IF EXISTS movies');
|
||||
|
||||
// 创建新表
|
||||
await db.execute('''
|
||||
CREATE TABLE movies (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
poster_path TEXT,
|
||||
release_date TEXT,
|
||||
directors TEXT,
|
||||
writers TEXT,
|
||||
actors TEXT,
|
||||
genres TEXT,
|
||||
alternate_titles TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'movie',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 迁移旧数据(尽可能保留)
|
||||
for (final row in oldData) {
|
||||
try {
|
||||
await db.insert('movies', {
|
||||
'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'title': row['title']?.toString() ?? '',
|
||||
'poster_path': row['poster_path'],
|
||||
'release_date': null,
|
||||
'directors': '[]',
|
||||
'writers': '[]',
|
||||
'actors': '[]',
|
||||
'genres': '[]',
|
||||
'alternate_titles': '[]',
|
||||
'summary': row['note'],
|
||||
'rating': row['rating'],
|
||||
'status': row['status'] ?? 'want_to_watch',
|
||||
'created_at': row['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
'is_deleted': row['is_deleted'] ?? 0,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[DB] 迁移影视记录失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建数据库表
|
||||
Future<void> _createDB(Database db, int version) async {
|
||||
// 影视表
|
||||
await db.execute('''
|
||||
CREATE TABLE movies (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
poster_path TEXT,
|
||||
release_date TEXT,
|
||||
directors TEXT,
|
||||
writers TEXT,
|
||||
actors TEXT,
|
||||
genres TEXT,
|
||||
alternate_titles TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'movie',
|
||||
watch_date TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
cover_offset REAL DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 书籍表
|
||||
await db.execute('''
|
||||
CREATE TABLE books (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
cover_path TEXT,
|
||||
authors TEXT,
|
||||
translators TEXT,
|
||||
alternate_titles TEXT,
|
||||
publisher TEXT,
|
||||
genres TEXT,
|
||||
summary TEXT,
|
||||
rating REAL,
|
||||
status TEXT NOT NULL,
|
||||
isbn TEXT,
|
||||
publish_date TEXT,
|
||||
start_date TEXT,
|
||||
finish_date TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
cover_offset REAL DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 笔记表
|
||||
await db.execute('''
|
||||
CREATE TABLE notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
content_type TEXT DEFAULT 'markdown',
|
||||
tags TEXT,
|
||||
images TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
is_pinned INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 影评表
|
||||
await db.execute('''
|
||||
CREATE TABLE movie_reviews (
|
||||
id TEXT PRIMARY KEY,
|
||||
movie_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 (movie_id) REFERENCES movies (id)
|
||||
)
|
||||
''');
|
||||
|
||||
// 影视海报墙表
|
||||
await db.execute('''
|
||||
CREATE TABLE movie_posters (
|
||||
id TEXT PRIMARY KEY,
|
||||
movie_id TEXT NOT NULL,
|
||||
poster_path TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
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)
|
||||
)
|
||||
''');
|
||||
|
||||
// 标签表
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
is_hidden INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(name, type)
|
||||
)
|
||||
''');
|
||||
|
||||
// EPUB 阅读器书籍表
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS reader_books (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT DEFAULT '',
|
||||
authors TEXT DEFAULT '',
|
||||
cover_path TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
file_extension TEXT NOT NULL DEFAULT 'epub',
|
||||
last_read_cfi TEXT DEFAULT '',
|
||||
reading_percentage REAL DEFAULT 0.0,
|
||||
book_id TEXT DEFAULT '',
|
||||
summary TEXT DEFAULT '',
|
||||
publisher TEXT DEFAULT '',
|
||||
isbn TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 书籍批注表
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS book_annotations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
cfi TEXT NOT NULL DEFAULT '',
|
||||
chapter TEXT DEFAULT '',
|
||||
type TEXT NOT NULL DEFAULT 'highlight',
|
||||
color TEXT NOT NULL DEFAULT 'FFEB3B',
|
||||
reader_note TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
// 关闭数据库
|
||||
Future close() async {
|
||||
if (_database != null) {
|
||||
await _database!.close();
|
||||
_database = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
237
lib/data/epub/reader_dao.dart
Normal file
237
lib/data/epub/reader_dao.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// EPUB 阅读器数据访问层
|
||||
class ReaderDao {
|
||||
final DatabaseHelper _db = DatabaseHelper.instance;
|
||||
|
||||
// ─── reader_books ─────────────────────────────────────────────────
|
||||
|
||||
/// 获取所有未删除的阅读记录(包含关联书籍封面)
|
||||
Future<List<Map<String, dynamic>>> getAllReaderBooks() async {
|
||||
final db = await _db.database;
|
||||
final results = await db.rawQuery('''
|
||||
SELECT rb.*,
|
||||
COALESCE(b.cover_path, rb.cover_path) as display_cover_path
|
||||
FROM reader_books rb
|
||||
LEFT JOIN books b ON rb.book_id = b.id AND b.is_deleted = 0
|
||||
WHERE rb.is_deleted = 0
|
||||
ORDER BY rb.updated_at DESC
|
||||
''');
|
||||
return results.map((row) {
|
||||
final map = Map<String, dynamic>.from(row);
|
||||
// 如果有关联封面,覆盖 cover_path 供 UI 使用
|
||||
final displayCover = map['display_cover_path'] as String?;
|
||||
if (displayCover != null && displayCover.isNotEmpty) {
|
||||
map['cover_path'] = displayCover;
|
||||
}
|
||||
map.remove('display_cover_path');
|
||||
return map;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 根据 ID 获取阅读记录
|
||||
Future<Map<String, dynamic>?> getReaderBookById(String id) async {
|
||||
final db = await _db.database;
|
||||
final results = await db.query(
|
||||
'reader_books',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
limit: 1,
|
||||
);
|
||||
return results.isNotEmpty ? results.first : null;
|
||||
}
|
||||
|
||||
/// 插入阅读记录
|
||||
Future<int> insertReaderBook(Map<String, dynamic> book) async {
|
||||
final db = await _db.database;
|
||||
return db.insert('reader_books', book);
|
||||
}
|
||||
|
||||
/// 更新阅读记录字段
|
||||
Future<int> updateReaderBook(String id, Map<String, dynamic> fields) async {
|
||||
final db = await _db.database;
|
||||
return db.update(
|
||||
'reader_books',
|
||||
fields,
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 更新阅读进度
|
||||
Future<int> updateReadingProgress(
|
||||
String id, String cfi, double percentage) async {
|
||||
final db = await _db.database;
|
||||
return db.update(
|
||||
'reader_books',
|
||||
{
|
||||
'last_read_cfi': cfi,
|
||||
'reading_percentage': percentage,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
/// 软删除
|
||||
Future<int> deleteReaderBook(String id) async {
|
||||
final db = await _db.database;
|
||||
return db.update(
|
||||
'reader_books',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 关联 books 表 ─────────────────────────────────────────────
|
||||
|
||||
/// 关联 EPUB 到 books 表中的书籍
|
||||
Future<int> linkToBook(String readerBookId, String bookId) async {
|
||||
final db = await _db.database;
|
||||
return db.update(
|
||||
'reader_books',
|
||||
{'book_id': bookId, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [readerBookId],
|
||||
);
|
||||
}
|
||||
|
||||
/// 取消关联
|
||||
Future<int> unlinkBook(String readerBookId) async {
|
||||
final db = await _db.database;
|
||||
// 同步删除该书的摘抄蓝色高亮标注
|
||||
await db.delete(
|
||||
'book_annotations',
|
||||
where: 'book_id = ? AND color = ?',
|
||||
whereArgs: [readerBookId, 'excerpt'],
|
||||
);
|
||||
return db.update(
|
||||
'reader_books',
|
||||
{'book_id': '', 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [readerBookId],
|
||||
);
|
||||
}
|
||||
|
||||
/// 通过 books.id 查找关联的 reader_book
|
||||
Future<Map<String, dynamic>?> getReaderBookByBookId(String bookId) async {
|
||||
final db = await _db.database;
|
||||
final results = await db.query(
|
||||
'reader_books',
|
||||
where: 'book_id = ? AND is_deleted = 0',
|
||||
whereArgs: [bookId],
|
||||
limit: 1,
|
||||
);
|
||||
return results.isNotEmpty ? results.first : null;
|
||||
}
|
||||
|
||||
// ─── book_annotations ─────────────────────────────────────────────
|
||||
|
||||
/// 获取某本书的所有批注
|
||||
Future<List<Map<String, dynamic>>> getAnnotationsByBookId(
|
||||
String bookId) async {
|
||||
final db = await _db.database;
|
||||
return db.query(
|
||||
'book_annotations',
|
||||
where: 'book_id = ?',
|
||||
whereArgs: [bookId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
}
|
||||
|
||||
/// 插入批注
|
||||
Future<int> insertAnnotation(Map<String, dynamic> annotation) async {
|
||||
final db = await _db.database;
|
||||
return db.insert('book_annotations', annotation);
|
||||
}
|
||||
|
||||
/// 删除批注
|
||||
Future<int> deleteAnnotation(int id) async {
|
||||
final db = await _db.database;
|
||||
return db.delete('book_annotations', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
/// 更新批注感悟
|
||||
Future<int> updateAnnotationNote(int id, String note) async {
|
||||
final db = await _db.database;
|
||||
return db.update(
|
||||
'book_annotations',
|
||||
{'reader_note': note, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
// ─── highlights / annotations (epub) ──────────────────────
|
||||
|
||||
/// 保存 EPUB 高亮批注
|
||||
Future<int> saveHighlight(Map<String, dynamic> annotation) async {
|
||||
final db = await _db.database;
|
||||
return db.insert('book_annotations', {
|
||||
...annotation,
|
||||
'type': 'highlight',
|
||||
});
|
||||
}
|
||||
|
||||
/// 获取某 book_id(books.id)的所有 EPUB 高亮
|
||||
Future<List<Map<String, dynamic>>> getHighlightsByBookId(
|
||||
String bookId) async {
|
||||
final db = await _db.database;
|
||||
return db.query(
|
||||
'book_annotations',
|
||||
where: 'book_id = ? AND type = ?',
|
||||
whereArgs: [bookId, 'highlight'],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
}
|
||||
|
||||
/// 删除高亮
|
||||
Future<int> deleteHighlight(int id) async {
|
||||
final db = await _db.database;
|
||||
return db.delete(
|
||||
'book_annotations',
|
||||
where: 'id = ? AND type = ?',
|
||||
whereArgs: [id, 'highlight'],
|
||||
);
|
||||
}
|
||||
|
||||
/// 删除摘抄对应的蓝色高亮标注(通过内容匹配)
|
||||
Future<void> deleteExcerptHighlightByContent(String readerBookId, String content) async {
|
||||
final db = await _db.database;
|
||||
await db.delete(
|
||||
'book_annotations',
|
||||
where: 'book_id = ? AND content = ? AND color = ?',
|
||||
whereArgs: [readerBookId, content, 'excerpt'],
|
||||
);
|
||||
}
|
||||
|
||||
// ─── bookmarks ──────────────────────────────────────────────────
|
||||
|
||||
/// 获取某本书的所有书签
|
||||
Future<List<Map<String, dynamic>>> getBookmarksByBookId(String bookId) async {
|
||||
final db = await _db.database;
|
||||
return db.query(
|
||||
'book_annotations',
|
||||
where: 'book_id = ? AND type = ?',
|
||||
whereArgs: [bookId, 'bookmark'],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
}
|
||||
|
||||
/// 插入书签
|
||||
Future<int> insertBookmark(Map<String, dynamic> bookmark) async {
|
||||
final db = await _db.database;
|
||||
return db.insert('book_annotations', {
|
||||
...bookmark,
|
||||
'type': 'bookmark',
|
||||
});
|
||||
}
|
||||
|
||||
/// 删除书签
|
||||
Future<int> deleteBookmark(int id) async {
|
||||
final db = await _db.database;
|
||||
return db.delete('book_annotations', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
}
|
||||
74
lib/data/epub/reader_models.dart
Normal file
74
lib/data/epub/reader_models.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
/// EPUB 解析结果数据模型
|
||||
library;
|
||||
|
||||
class EpubBookInfo {
|
||||
final String title;
|
||||
final String author;
|
||||
final List<String> authors;
|
||||
final String? description;
|
||||
final String? coverHref;
|
||||
final String opfRootPath;
|
||||
final String epubVersion;
|
||||
final List<SpineItem> spine;
|
||||
final List<TocEntry> toc;
|
||||
|
||||
EpubBookInfo({
|
||||
required this.title,
|
||||
required this.author,
|
||||
required this.authors,
|
||||
this.description,
|
||||
this.coverHref,
|
||||
required this.opfRootPath,
|
||||
required this.epubVersion,
|
||||
required this.spine,
|
||||
required this.toc,
|
||||
});
|
||||
}
|
||||
|
||||
class SpineItem {
|
||||
final int index;
|
||||
final String href;
|
||||
final String idref;
|
||||
final bool linear;
|
||||
|
||||
SpineItem({
|
||||
required this.index,
|
||||
required this.href,
|
||||
required this.idref,
|
||||
this.linear = true,
|
||||
});
|
||||
}
|
||||
|
||||
class TocEntry {
|
||||
final String label;
|
||||
final String href;
|
||||
final int spineIndex;
|
||||
final List<TocEntry> children;
|
||||
|
||||
TocEntry({
|
||||
required this.label,
|
||||
required this.href,
|
||||
this.spineIndex = -1,
|
||||
this.children = const [],
|
||||
});
|
||||
|
||||
/// 递归展平为列表(保留层级信息通过 depth)
|
||||
List<FlatTocItem> flatten() {
|
||||
final result = <FlatTocItem>[];
|
||||
_flattenRecursive(result, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _flattenRecursive(List<FlatTocItem> list, int depth) {
|
||||
list.add(FlatTocItem(entry: this, depth: depth));
|
||||
for (final child in children) {
|
||||
child._flattenRecursive(list, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FlatTocItem {
|
||||
final TocEntry entry;
|
||||
final int depth;
|
||||
FlatTocItem({required this.entry, required this.depth});
|
||||
}
|
||||
265
lib/data/movie/movie_dao.dart
Normal file
265
lib/data/movie/movie_dao.dart
Normal file
@@ -0,0 +1,265 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 影视数据访问对象
|
||||
class MovieDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[MovieDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有影视记录(未删除的)
|
||||
Future<List<Movie>> getAllMovies() => _wrap('getAllMovies', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 分页查询影视记录
|
||||
Future<List<Movie>> getMoviesPaged({String? status, int limit = 20, int offset = 0, int sortMode = 0}) => _wrap('getMoviesPaged', () async {
|
||||
final db = await _dbHelper.database;
|
||||
String where = 'is_deleted = 0';
|
||||
List<dynamic> whereArgs = [];
|
||||
if (status != null && status.isNotEmpty) {
|
||||
where += ' AND status = ?';
|
||||
whereArgs.add(status);
|
||||
}
|
||||
final maps = await db.query('movies', where: where, whereArgs: whereArgs,
|
||||
orderBy: _buildMovieOrderBy(sortMode), limit: limit, offset: offset);
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
static String _buildMovieOrderBy(int sortMode) {
|
||||
switch (sortMode) {
|
||||
case 1: return 'created_at DESC';
|
||||
case 2: return 'rating DESC NULLS LAST, updated_at DESC';
|
||||
default: return 'updated_at DESC';
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态筛选影视记录
|
||||
Future<List<Movie>> getMoviesByStatus(String status) => _wrap('getMoviesByStatus', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'status = ? AND is_deleted = ?',
|
||||
whereArgs: [status, 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 根据导演筛选
|
||||
Future<List<Movie>> getMoviesByDirector(String director) => _wrap('getMoviesByDirector', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'directors LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$director%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.directors.contains(director))
|
||||
.toList();
|
||||
});
|
||||
|
||||
// 根据编剧筛选
|
||||
Future<List<Movie>> getMoviesByWriter(String writer) => _wrap('getMoviesByWriter', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'writers LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$writer%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.writers.contains(writer))
|
||||
.toList();
|
||||
});
|
||||
|
||||
// 根据演员筛选
|
||||
Future<List<Movie>> getMoviesByActor(String actor) => _wrap('getMoviesByActor', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'actors LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$actor%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.actors.contains(actor))
|
||||
.toList();
|
||||
});
|
||||
|
||||
// 根据类型筛选
|
||||
Future<List<Movie>> getMoviesByGenre(String genre) => _wrap('getMoviesByGenre', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'genres LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$genre%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.genres.contains(genre))
|
||||
.toList();
|
||||
});
|
||||
|
||||
// 搜索影视(标题或别名)
|
||||
Future<List<Movie>> searchMovies(String keyword) => _wrap('searchMovies', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final likeKeyword = '%$keyword%';
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: '(title LIKE ? OR alternate_titles LIKE ?) AND is_deleted = ?',
|
||||
whereArgs: [likeKeyword, likeKeyword, 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((m) => Movie.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 添加影视记录
|
||||
Future<int> insertMovie(Movie movie) => _wrap('insertMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movies', movie.toJson());
|
||||
});
|
||||
|
||||
// 更新影视记录
|
||||
Future<int> updateMovie(Movie movie) => _wrap('updateMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movies',
|
||||
movie.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [movie.id],
|
||||
);
|
||||
});
|
||||
|
||||
// 仅更新封面偏移量(不触发全量刷新)
|
||||
Future<void> updateCoverOffset(String movieId, double offset) => _wrap('updateCoverOffset', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update('movies', {'cover_offset': offset}, where: 'id = ?', whereArgs: [movieId]);
|
||||
});
|
||||
|
||||
// 删除影视记录(软删除)
|
||||
Future<int> deleteMovie(String id) => _wrap('deleteMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movies',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
// 获取所有导演(去重)
|
||||
Future<List<String>> getAllDirectors() => _wrap('getAllDirectors', () async {
|
||||
final movies = await getAllMovies();
|
||||
final directors = <String>{};
|
||||
for (final movie in movies) {
|
||||
directors.addAll(movie.directors);
|
||||
}
|
||||
return directors.toList()..sort();
|
||||
});
|
||||
|
||||
// 获取所有编剧(去重)
|
||||
Future<List<String>> getAllWriters() => _wrap('getAllWriters', () async {
|
||||
final movies = await getAllMovies();
|
||||
final writers = <String>{};
|
||||
for (final movie in movies) {
|
||||
writers.addAll(movie.writers);
|
||||
}
|
||||
return writers.toList()..sort();
|
||||
});
|
||||
|
||||
// 获取所有演员(去重)
|
||||
Future<List<String>> getAllActors() => _wrap('getAllActors', () async {
|
||||
final movies = await getAllMovies();
|
||||
final actors = <String>{};
|
||||
for (final movie in movies) {
|
||||
actors.addAll(movie.actors);
|
||||
}
|
||||
return actors.toList()..sort();
|
||||
});
|
||||
|
||||
// 获取所有类型(去重)
|
||||
Future<List<String>> getAllGenres() => _wrap('getAllGenres', () async {
|
||||
final movies = await getAllMovies();
|
||||
final genres = <String>{};
|
||||
for (final movie in movies) {
|
||||
genres.addAll(movie.genres);
|
||||
}
|
||||
return genres.toList()..sort();
|
||||
});
|
||||
|
||||
// 一次性获取所有元数据(导演、编剧、演员、类型)
|
||||
Future<Map<String, List<String>>> getAllMetadata() => _wrap('getAllMetadata', () async {
|
||||
final movies = await getAllMovies();
|
||||
final directors = <String>{};
|
||||
final writers = <String>{};
|
||||
final actors = <String>{};
|
||||
final genres = <String>{};
|
||||
for (final movie in movies) {
|
||||
directors.addAll(movie.directors);
|
||||
writers.addAll(movie.writers);
|
||||
actors.addAll(movie.actors);
|
||||
genres.addAll(movie.genres);
|
||||
}
|
||||
return {
|
||||
'directors': directors.toList()..sort(),
|
||||
'writers': writers.toList()..sort(),
|
||||
'actors': actors.toList()..sort(),
|
||||
'genres': genres.toList()..sort(),
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的影视
|
||||
Future<List<Movie>> getDeletedMovies() => _wrap('getDeletedMovies', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [1],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 恢复已删除的影视
|
||||
Future<int> restoreMovie(String id) => _wrap('restoreMovie', () 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) => _wrap('permanentDeleteMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
// 清理子记录,防止孤儿数据
|
||||
await db.delete('movie_reviews', where: 'movie_id = ?', whereArgs: [id]);
|
||||
await db.delete('movie_posters', where: 'movie_id = ?', whereArgs: [id]);
|
||||
return await db.delete(
|
||||
'movies',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
}
|
||||
68
lib/data/movie/movie_poster_dao.dart
Normal file
68
lib/data/movie/movie_poster_dao.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 影视海报墙数据访问对象
|
||||
class MoviePosterDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[MoviePosterDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取影视的所有海报
|
||||
Future<List<MoviePoster>> getPostersByMovieId(String movieId) => _wrap('getPostersByMovieId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_posters',
|
||||
where: 'movie_id = ? AND is_deleted = 0',
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => MoviePoster.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 根据ID获取海报
|
||||
Future<MoviePoster?> getPosterById(String id) => _wrap('getPosterById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_posters',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return MoviePoster.fromJson(maps.first);
|
||||
});
|
||||
|
||||
/// 添加海报
|
||||
Future<int> insertPoster(MoviePoster poster) => _wrap('insertPoster', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movie_posters', poster.toJson());
|
||||
});
|
||||
|
||||
/// 软删除海报
|
||||
Future<int> deletePoster(String id) => _wrap('deletePoster', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movie_posters',
|
||||
{'is_deleted': 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 获取影视的海报数量
|
||||
Future<int> getPosterCount(String movieId) => _wrap('getPosterCount', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM movie_posters WHERE movie_id = ? AND is_deleted = 0',
|
||||
[movieId],
|
||||
);
|
||||
return result.first['count'] as int? ?? 0;
|
||||
});
|
||||
}
|
||||
135
lib/data/movie/movie_review_dao.dart
Normal file
135
lib/data/movie/movie_review_dao.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 影评数据访问对象
|
||||
class MovieReviewDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[MovieReviewDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取影视的所有影评
|
||||
Future<List<MovieReview>> getReviewsByMovieId(String movieId) => _wrap('getReviewsByMovieId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
where: 'movie_id = ? AND is_deleted = 0',
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 根据ID获取影评
|
||||
Future<MovieReview?> getReviewById(String id) => _wrap('getReviewById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return MovieReview.fromJson(maps.first);
|
||||
});
|
||||
|
||||
/// 添加影评
|
||||
Future<int> insertReview(MovieReview review) => _wrap('insertReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movie_reviews', review.toJson());
|
||||
});
|
||||
|
||||
/// 更新影评
|
||||
Future<int> updateReview(MovieReview review) => _wrap('updateReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movie_reviews',
|
||||
review.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [review.id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 软删除影评
|
||||
Future<int> deleteReview(String id) => _wrap('deleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movie_reviews',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toUtc().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 获取影视的影评数量
|
||||
Future<int> getReviewCount(String movieId) => _wrap('getReviewCount', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM movie_reviews WHERE movie_id = ? AND is_deleted = 0',
|
||||
[movieId],
|
||||
);
|
||||
return result.first['count'] as int? ?? 0;
|
||||
});
|
||||
|
||||
/// 获取短评列表
|
||||
Future<List<MovieReview>> getShortReviews(String movieId) => _wrap('getShortReviews', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
where: 'movie_id = ? AND review_type = 1 AND is_deleted = 0',
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 获取长评列表
|
||||
Future<List<MovieReview>> getLongReviews(String movieId) => _wrap('getLongReviews', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
where: 'movie_id = ? AND review_type = 2 AND is_deleted = 0',
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 获取所有已删除的影评
|
||||
Future<List<MovieReview>> getDeletedReviews() => _wrap('getDeletedReviews', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
where: 'is_deleted = 1',
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 恢复已删除的影评
|
||||
Future<void> restoreReview(String id) => _wrap('restoreReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'movie_reviews',
|
||||
{'is_deleted': 0},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
/// 彻底删除影评
|
||||
Future<void> permanentDeleteReview(String id) => _wrap('permanentDeleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'movie_reviews',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
}
|
||||
156
lib/data/note/note_dao.dart
Normal file
156
lib/data/note/note_dao.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
/// 笔记数据访问对象
|
||||
class NoteDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[NoteDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有未删除的笔记
|
||||
Future<List<Note>> getAllNotes({int sortMode = 0}) => _wrap('getAllNotes', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
orderBy: _buildOrderBy(sortMode),
|
||||
);
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 分页查询笔记
|
||||
Future<List<Note>> getNotesPaged({int limit = 20, int offset = 0, int sortMode = 0}) => _wrap('getNotesPaged', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query('notes', where: 'is_deleted = 0',
|
||||
orderBy: _buildOrderBy(sortMode), limit: limit, offset: offset);
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
/// 根据排序模式生成 ORDER BY 子句,置顶始终排最前
|
||||
static String _buildOrderBy(int sortMode) {
|
||||
switch (sortMode) {
|
||||
case 1: return 'is_pinned DESC, created_at DESC';
|
||||
case 2: return 'is_pinned DESC, title COLLATE NOCASE ASC';
|
||||
default: return 'is_pinned DESC, updated_at DESC';
|
||||
}
|
||||
}
|
||||
|
||||
// 根据ID获取笔记
|
||||
Future<Note?> getNoteById(String id) => _wrap('getNoteById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'id = ? AND is_deleted = ?',
|
||||
whereArgs: [id, 0],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return Note.fromJson(maps.first);
|
||||
});
|
||||
|
||||
// 添加笔记
|
||||
Future<int> insertNote(Note note) => _wrap('insertNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('notes', note.toJson());
|
||||
});
|
||||
|
||||
// 更新笔记
|
||||
Future<int> updateNote(Note note) => _wrap('updateNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
note.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [note.id],
|
||||
);
|
||||
});
|
||||
|
||||
// 软删除笔记
|
||||
Future<int> deleteNote(String id) => _wrap('deleteNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
{'is_deleted': 1, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
// 切换笔记置顶状态
|
||||
Future<int> togglePin(String id, bool isPinned) => _wrap('togglePin', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
{'is_pinned': isPinned ? 1 : 0, 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的笔记
|
||||
Future<List<Note>> getDeletedNotes() => _wrap('getDeletedNotes', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [1],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 恢复已删除的笔记
|
||||
Future<int> restoreNote(String id) => _wrap('restoreNote', () 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) => _wrap('permanentDeleteNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'notes',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
|
||||
// 搜索笔记
|
||||
Future<List<Note>> searchNotes(String query) => _wrap('searchNotes', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: '(title LIKE ? OR content LIKE ? OR tags LIKE ?) AND is_deleted = ?',
|
||||
whereArgs: ['%$query%', '%$query%', '%$query%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
});
|
||||
|
||||
// 根据标签筛选
|
||||
Future<List<Note>> getNotesByTag(String tag) => _wrap('getNotesByTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'tags LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$tag%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
});
|
||||
}
|
||||
259
lib/data/tag/tag_dao.dart
Normal file
259
lib/data/tag/tag_dao.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
class TagDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[TagDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定类型的所有标签,按名称排序
|
||||
/// [excludeHidden] 为 true 时,过滤掉隐藏标签
|
||||
Future<List<Map<String, dynamic>>> getTagsByType(String type, {bool excludeHidden = false}) => _wrap('getTagsByType', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final where = excludeHidden ? 'type = ? AND is_hidden = 0' : 'type = ?';
|
||||
return await db.query('tags',
|
||||
where: where,
|
||||
whereArgs: [type],
|
||||
orderBy: 'name ASC');
|
||||
});
|
||||
|
||||
/// 根据ID获取标签
|
||||
Future<Map<String, dynamic>?> getTagById(String id) => _wrap('getTagById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final results = await db.query('tags', where: 'id = ?', whereArgs: [id]);
|
||||
return results.isNotEmpty ? results.first : null;
|
||||
});
|
||||
|
||||
/// 添加标签,返回新标签ID
|
||||
Future<String> addTag(String name, String type) => _wrap('addTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final id = 'tag_${DateTime.now().millisecondsSinceEpoch}';
|
||||
await db.insert('tags', {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
return id;
|
||||
});
|
||||
|
||||
/// 重命名标签,同时级联更新所有关联条目(事务保护)
|
||||
Future<bool> renameTag(String tagId, String newName) => _wrap('renameTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
|
||||
final tag = await getTagById(tagId);
|
||||
if (tag == null) return false;
|
||||
|
||||
final oldName = tag['name'] as String;
|
||||
final type = tag['type'] as String;
|
||||
if (oldName == newName) return true;
|
||||
|
||||
// 检查新名称是否已存在同类型标签
|
||||
final existing = await db.query('tags',
|
||||
where: 'name = ? AND type = ? AND id != ?',
|
||||
whereArgs: [newName, type, tagId]);
|
||||
if (existing.isNotEmpty) return false;
|
||||
|
||||
await db.transaction((txn) async {
|
||||
await txn.update('tags', {'name': newName},
|
||||
where: 'id = ?', whereArgs: [tagId]);
|
||||
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeRenameInMovies(txn, oldName, newName);
|
||||
case 'book_genre':
|
||||
await _cascadeRenameInBooks(txn, oldName, newName);
|
||||
case 'note_tag':
|
||||
await _cascadeRenameInNotes(txn, oldName, newName);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
/// 删除标签(事务保护)
|
||||
/// [replacementName] 不为 null 时,先将所有条目中的旧标签替换为新标签,再删除
|
||||
/// [replacementName] 为 null 时,从所有条目中移除该标签
|
||||
Future<void> deleteTag(String tagId, {String? replacementName}) => _wrap('deleteTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
|
||||
final tag = await getTagById(tagId);
|
||||
if (tag == null) return;
|
||||
|
||||
final name = tag['name'] as String;
|
||||
final type = tag['type'] as String;
|
||||
|
||||
await db.transaction((txn) async {
|
||||
if (replacementName != null && replacementName != name) {
|
||||
await _ensureTagExists(txn, replacementName, type);
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeRenameInMovies(txn, name, replacementName);
|
||||
case 'book_genre':
|
||||
await _cascadeRenameInBooks(txn, name, replacementName);
|
||||
case 'note_tag':
|
||||
await _cascadeRenameInNotes(txn, name, replacementName);
|
||||
}
|
||||
} else if (replacementName == null) {
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeDeleteFromMovies(txn, name);
|
||||
case 'book_genre':
|
||||
await _cascadeDeleteFromBooks(txn, name);
|
||||
case 'note_tag':
|
||||
await _cascadeDeleteFromNotes(txn, name);
|
||||
}
|
||||
}
|
||||
|
||||
await txn.delete('tags', where: 'id = ?', whereArgs: [tagId]);
|
||||
});
|
||||
});
|
||||
|
||||
/// 仅删除标签本身,不级联影响已有条目(标签名保留在条目上)
|
||||
Future<void> deleteTagOnly(String tagId) => _wrap('deleteTagOnly', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete('tags', where: 'id = ?', whereArgs: [tagId]);
|
||||
});
|
||||
|
||||
/// 切换标签的隐藏状态
|
||||
Future<void> toggleHidden(String tagId) => _wrap('toggleHidden', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.rawUpdate('UPDATE tags SET is_hidden = 1 - is_hidden WHERE id = ?', [tagId]);
|
||||
});
|
||||
|
||||
/// 确保标签存在(用于替换操作)
|
||||
Future<void> _ensureTagExists(Transaction txn, String name, String type) async {
|
||||
final existing = await txn.query('tags',
|
||||
where: 'name = ? AND type = ?', whereArgs: [name, type]);
|
||||
if (existing.isEmpty) {
|
||||
final id = 'tag_${DateTime.now().millisecondsSinceEpoch}';
|
||||
await txn.insert('tags', {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 级联重命名 ======
|
||||
|
||||
Future<void> _cascadeRenameInMovies(Transaction txn, String oldName, String newName) async {
|
||||
final movies = await txn.query('movies',
|
||||
where: "genres LIKE ?", whereArgs: ['%$oldName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in movies) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (!genres.contains(oldName)) continue;
|
||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||
await txn.update('movies', {
|
||||
'genres': jsonEncode(updated),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeRenameInBooks(Transaction txn, String oldName, String newName) async {
|
||||
final books = await txn.query('books',
|
||||
where: "genres LIKE ?", whereArgs: ['%$oldName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in books) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (!genres.contains(oldName)) continue;
|
||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||
await txn.update('books', {
|
||||
'genres': jsonEncode(updated),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeRenameInNotes(Transaction txn, String oldName, String newName) async {
|
||||
final notes = await txn.query('notes',
|
||||
where: "tags LIKE ?", whereArgs: ['%$oldName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in notes) {
|
||||
final tags = _parseList(row['tags']);
|
||||
if (!tags.contains(oldName)) continue;
|
||||
final updated = tags.map((t) => t == oldName ? newName : t).toList();
|
||||
await txn.update('notes', {
|
||||
'tags': jsonEncode(updated),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 级联删除 ======
|
||||
|
||||
Future<void> _cascadeDeleteFromMovies(Transaction txn, String tagName) async {
|
||||
final movies = await txn.query('movies',
|
||||
where: "genres LIKE ?", whereArgs: ['%$tagName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in movies) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (!genres.contains(tagName)) continue;
|
||||
genres.removeWhere((g) => g == tagName);
|
||||
await txn.update('movies', {
|
||||
'genres': jsonEncode(genres),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeDeleteFromBooks(Transaction txn, String tagName) async {
|
||||
final books = await txn.query('books',
|
||||
where: "genres LIKE ?", whereArgs: ['%$tagName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in books) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (!genres.contains(tagName)) continue;
|
||||
genres.removeWhere((g) => g == tagName);
|
||||
await txn.update('books', {
|
||||
'genres': jsonEncode(genres),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeDeleteFromNotes(Transaction txn, String tagName) async {
|
||||
final notes = await txn.query('notes',
|
||||
where: "tags LIKE ?", whereArgs: ['%$tagName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in notes) {
|
||||
final tags = _parseList(row['tags']);
|
||||
if (!tags.contains(tagName)) continue;
|
||||
tags.removeWhere((t) => t == tagName);
|
||||
await txn.update('notes', {
|
||||
'tags': jsonEncode(tags),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 JSON 字符串列表
|
||||
List<String> _parseList(dynamic data) {
|
||||
if (data == null) return [];
|
||||
if (data is List) return data.map((e) => e.toString()).toList();
|
||||
if (data is String) {
|
||||
if (data.isEmpty || data == '[]') return [];
|
||||
try {
|
||||
final decoded = jsonDecode(data);
|
||||
if (decoded is List) {
|
||||
return decoded.map((e) => e.toString()).toList();
|
||||
}
|
||||
} catch (_) {
|
||||
return data.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user