This commit is contained in:
DelLevin-Home
2026-06-18 18:52:01 +08:00
parent ba2612fef8
commit 8c7631b252
23 changed files with 906 additions and 599 deletions

View File

@@ -1,27 +1,38 @@
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;
}
}
/// 获取指定类型的所有标签,按名称排序
Future<List<Map<String, dynamic>>> getTagsByType(String type) async {
Future<List<Map<String, dynamic>>> getTagsByType(String type) => _wrap('getTagsByType', () async {
final db = await _dbHelper.database;
return await db.query('tags',
where: 'type = ?',
whereArgs: [type],
orderBy: 'name ASC');
}
});
/// 根据ID获取标签
Future<Map<String, dynamic>?> getTagById(String id) async {
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) async {
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', {
@@ -31,10 +42,10 @@ class TagDao {
'created_at': DateTime.now().toIso8601String(),
});
return id;
}
});
/// 重命名标签,同时级联更新所有关联条目
Future<bool> renameTag(String tagId, String newName) async {
/// 重命名标签,同时级联更新所有关联条目(事务保护)
Future<bool> renameTag(String tagId, String newName) => _wrap('renameTag', () async {
final db = await _dbHelper.database;
final tag = await getTagById(tagId);
@@ -50,26 +61,27 @@ class TagDao {
whereArgs: [newName, type, tagId]);
if (existing.isNotEmpty) return false;
await db.update('tags', {'name': newName},
where: 'id = ?', whereArgs: [tagId]);
await db.transaction((txn) async {
await txn.update('tags', {'name': newName},
where: 'id = ?', whereArgs: [tagId]);
// 级联更新
switch (type) {
case 'movie_genre':
await _cascadeRenameInMovies(oldName, newName);
case 'book_genre':
await _cascadeRenameInBooks(oldName, newName);
case 'note_tag':
await _cascadeRenameInNotes(oldName, newName);
}
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}) async {
Future<void> deleteTag(String tagId, {String? replacementName}) => _wrap('deleteTag', () async {
final db = await _dbHelper.database;
final tag = await getTagById(tagId);
@@ -78,137 +90,144 @@ class TagDao {
final name = tag['name'] as String;
final type = tag['type'] as String;
if (replacementName != null && replacementName != name) {
await _ensureTagExists(replacementName, type);
switch (type) {
case 'movie_genre':
await _cascadeRenameInMovies(name, replacementName);
case 'book_genre':
await _cascadeRenameInBooks(name, replacementName);
case 'note_tag':
await _cascadeRenameInNotes(name, replacementName);
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);
}
}
} else if (replacementName == null) {
switch (type) {
case 'movie_genre':
await _cascadeDeleteFromMovies(name);
case 'book_genre':
await _cascadeDeleteFromBooks(name);
case 'note_tag':
await _cascadeDeleteFromNotes(name);
}
}
await db.delete('tags', where: 'id = ?', whereArgs: [tagId]);
}
await txn.delete('tags', where: 'id = ?', whereArgs: [tagId]);
});
});
/// 仅删除标签本身,不级联影响已有条目(标签名保留在条目上)
Future<void> deleteTagOnly(String tagId) async {
Future<void> deleteTagOnly(String tagId) => _wrap('deleteTagOnly', () async {
final db = await _dbHelper.database;
await db.delete('tags', where: 'id = ?', whereArgs: [tagId]);
}
});
/// 确保标签存在(用于替换操作)
Future<void> _ensureTagExists(String name, String type) async {
final db = await _dbHelper.database;
final existing = await db.query('tags',
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) {
await addTag(name, type);
final id = 'tag_${DateTime.now().millisecondsSinceEpoch}';
await txn.insert('tags', {
'id': id,
'name': name,
'type': type,
'created_at': DateTime.now().toIso8601String(),
});
}
}
// ====== 级联重命名 ======
Future<void> _cascadeRenameInMovies(String oldName, String newName) async {
final db = await _dbHelper.database;
final movies = await db.query('movies');
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) && !genres.contains(newName)) {
final updated = genres.map((g) => g == oldName ? newName : g).toList();
await db.update('movies', {
'genres': jsonEncode(updated),
'updated_at': DateTime.now().toIso8601String(),
}, where: 'id = ?', whereArgs: [row['id']]);
}
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(String oldName, String newName) async {
final db = await _dbHelper.database;
final books = await db.query('books');
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) && !genres.contains(newName)) {
final updated = genres.map((g) => g == oldName ? newName : g).toList();
await db.update('books', {
'genres': jsonEncode(updated),
'updated_at': DateTime.now().toIso8601String(),
}, where: 'id = ?', whereArgs: [row['id']]);
}
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(String oldName, String newName) async {
final db = await _dbHelper.database;
final notes = await db.query('notes');
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) && !tags.contains(newName)) {
final updated = tags.map((t) => t == oldName ? newName : t).toList();
await db.update('notes', {
'tags': jsonEncode(updated),
'updated_at': DateTime.now().toIso8601String(),
}, where: 'id = ?', whereArgs: [row['id']]);
}
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(String tagName) async {
final db = await _dbHelper.database;
final movies = await db.query('movies');
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)) {
genres.removeWhere((g) => g == tagName);
await db.update('movies', {
'genres': jsonEncode(genres),
'updated_at': DateTime.now().toIso8601String(),
}, where: 'id = ?', whereArgs: [row['id']]);
}
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(String tagName) async {
final db = await _dbHelper.database;
final books = await db.query('books');
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)) {
genres.removeWhere((g) => g == tagName);
await db.update('books', {
'genres': jsonEncode(genres),
'updated_at': DateTime.now().toIso8601String(),
}, where: 'id = ?', whereArgs: [row['id']]);
}
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(String tagName) async {
final db = await _dbHelper.database;
final notes = await db.query('notes');
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)) {
tags.removeWhere((t) => t == tagName);
await db.update('notes', {
'tags': jsonEncode(tags),
'updated_at': DateTime.now().toIso8601String(),
}, where: 'id = ?', whereArgs: [row['id']]);
}
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']]);
}
}