优化项目结构

This commit is contained in:
DelLevin-Home
2026-07-05 13:58:25 +08:00
parent 82aca12402
commit de180eee47
41 changed files with 0 additions and 0 deletions

186
lib/data/book/book_dao.dart Normal file
View 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],
);
});
}

View 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],
);
});
}

View 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],
);
});
}

View 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;
}
}
}

View 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_idbooks.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]);
}
}

View 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});
}

View 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],
);
});
}

View 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;
});
}

View 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
View 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
View 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 [];
}
}