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

@@ -233,6 +233,7 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
return;
}
try {
final now = DateTime.now();
if (widget.review == null) {
@@ -264,5 +265,9 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
Navigator.pop(context);
} catch (e) {
if (!mounted) return;
ToastUtil.show(context, '保存失败: $e');
}
}
}

View File

@@ -28,6 +28,7 @@ class _BookTabPageState extends State<BookTabPage> {
int _lastDataCount = -1;
DateTime? _lastUpdatedAt;
late ScrollController _scrollController;
AppProvider? _provider;
static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
@@ -38,6 +39,7 @@ class _BookTabPageState extends State<BookTabPage> {
_scrollController = ScrollController()..addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<AppProvider>();
_provider = provider;
provider.addListener(_onDataChanged);
_lastDataCount = provider.books.length;
if (provider.books.isNotEmpty) _lastUpdatedAt = provider.books.first.updatedAt;
@@ -47,6 +49,7 @@ class _BookTabPageState extends State<BookTabPage> {
@override
void dispose() {
_provider?.removeListener(_onDataChanged);
_scrollController.dispose();
super.dispose();
}

View File

@@ -1806,6 +1806,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
return;
}
try {
// 收集所有多值字段输入框中的未提交内容
_collectUnsubmittedValues();
@@ -1869,6 +1870,10 @@ class _MovieFormPageState extends State<MovieFormPage> {
ToastUtil.show(context, widget.movie == null ? '添加成功' : '更新成功');
Navigator.pop(context);
} catch (e) {
if (!mounted) return;
ToastUtil.show(context, '保存失败: $e');
}
}
/// 将海报从临时ID目录移动到新的影视ID目录

View File

@@ -303,6 +303,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
Future<void> _saveReview() async {
if (!_formKey.currentState!.validate()) return;
try {
final now = DateTime.now();
if (widget.review == null) {
@@ -331,5 +332,9 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
if (!mounted) return;
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
Navigator.pop(context);
} catch (e) {
if (!mounted) return;
ToastUtil.show(context, '保存失败: $e');
}
}
}

View File

@@ -28,6 +28,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
int _lastDataCount = -1;
DateTime? _lastUpdatedAt;
late ScrollController _scrollController;
AppProvider? _provider;
static const _statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
@@ -38,6 +39,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
_scrollController = ScrollController()..addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<AppProvider>();
_provider = provider;
provider.addListener(_onDataChanged);
_lastDataCount = provider.movies.length;
if (provider.movies.isNotEmpty) _lastUpdatedAt = provider.movies.first.updatedAt;
@@ -47,6 +49,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
@override
void dispose() {
_provider?.removeListener(_onDataChanged);
_scrollController.dispose();
super.dispose();
}

View File

@@ -825,6 +825,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
return;
}
try {
final now = DateTime.now();
if (_isEditing) {
@@ -873,6 +874,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
if (!mounted) return;
Navigator.pop(context);
} catch (e) {
if (!mounted) return;
ToastUtil.show(context, '保存失败: $e');
}
}
/// 将图片从临时ID目录移动到新ID目录

View File

@@ -21,6 +21,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
bool _isLoading = false;
int _offset = 0;
late ScrollController _scrollController;
AppProvider? _provider;
int _layoutStyle = 0;
bool _initialized = false;
int _lastDataCount = -1;
@@ -33,6 +34,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
_scrollController = ScrollController()..addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<AppProvider>();
_provider = provider;
provider.addListener(_onDataChanged);
_lastDataCount = provider.notes.length;
if (provider.notes.isNotEmpty) _lastUpdatedAt = provider.notes.first.updatedAt;
@@ -42,6 +44,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
@override
void dispose() {
_provider?.removeListener(_onDataChanged);
_scrollController.dispose();
super.dispose();
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../models/data_models.dart';
import 'slide_up_page_route.dart';
import '../pages/movies/movie_form_page.dart';
import '../pages/book/book_form_page.dart';
import '../pages/note/note_form_page.dart';
@@ -7,7 +8,6 @@ import '../pages/movies/movie_detail_page.dart';
import '../pages/book/book_detail_page.dart';
import '../pages/note/note_detail_page.dart';
import '../pages/movies/douban_webview_page.dart';
import 'slide_up_page_route.dart';
/// 路由生成器
class AppRouter {
@@ -15,56 +15,65 @@ class AppRouter {
switch (settings.name) {
case '/movie-form':
final args = settings.arguments;
Movie? movie;
String? initialStatus;
if (args is Movie) {
movie = args;
} else if (args is Map<String, dynamic>) {
initialStatus = args['initialStatus'] as String?;
}
final Movie? movie = args is Movie ? args : null;
final String? initialStatus =
args is Map<String, dynamic> ? (args['initialStatus'] as String?) : null;
return SlideUpPageRoute(
page: MovieFormPage(movie: movie, initialStatus: initialStatus),
);
case '/book-form':
final args = settings.arguments;
Book? book;
String? initialStatus;
if (args is Book) {
book = args;
} else if (args is Map<String, dynamic>) {
initialStatus = args['initialStatus'] as String?;
}
final Book? book = args is Book ? args : null;
final String? initialStatus =
args is Map<String, dynamic> ? (args['initialStatus'] as String?) : null;
return SlideUpPageRoute(
page: BookFormPage(book: book, initialStatus: initialStatus),
);
case '/note-form':
final note = settings.arguments as Note?;
final args = settings.arguments;
final Note? note = args is Note ? args : null;
return SlideUpPageRoute(page: NoteFormPage(note: note));
case '/movie-detail':
final movie = settings.arguments as Movie;
final movie = settings.arguments is Movie ? settings.arguments as Movie : null;
if (movie == null) {
return _buildUnknownRoute(settings.name);
}
return SlideUpPageRoute(page: MovieDetailPage(movie: movie));
case '/book-detail':
final book = settings.arguments as Book;
final book = settings.arguments is Book ? settings.arguments as Book : null;
if (book == null) {
return _buildUnknownRoute(settings.name);
}
return SlideUpPageRoute(page: BookDetailPage(book: book));
case '/note-detail':
final note = settings.arguments as Note;
final note = settings.arguments is Note ? settings.arguments as Note : null;
if (note == null) {
return _buildUnknownRoute(settings.name);
}
return SlideUpPageRoute(page: NoteDetailPage(note: note));
case '/douban-webview':
final url = settings.arguments as String;
final url = settings.arguments is String ? settings.arguments as String : null;
if (url == null) {
return _buildUnknownRoute(settings.name);
}
return SlideUpPageRoute(page: DoubanWebViewPage(url: url));
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(child: Text('未找到页面:${settings.name}')),
),
);
return _buildUnknownRoute(settings.name);
}
}
static Route<dynamic> _buildUnknownRoute(String? name) {
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(child: Text('未找到页面:${name ?? ''}')),
),
);
}
}

View File

@@ -1,4 +1,4 @@
import 'package:sqflite/sqflite.dart';
import 'package:flutter/foundation.dart';
import '../../models/data_models.dart';
import '../database_helper.dart';
@@ -6,8 +6,17 @@ 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() async {
Future<List<Book>> getAllBooks() => _wrap('getAllBooks', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
@@ -15,12 +24,11 @@ class BookDao {
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}) async {
Future<List<Book>> getBooksPaged({String? status, int limit = 20, int offset = 0}) => _wrap('getBooksPaged', () async {
final db = await _dbHelper.database;
String where = 'is_deleted = 0';
List<dynamic> whereArgs = [];
@@ -31,10 +39,10 @@ class BookDao {
final maps = await db.query('books', where: where, whereArgs: whereArgs,
orderBy: 'created_at DESC', limit: limit, offset: offset);
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
});
// 根据状态筛选书籍记录
Future<List<Book>> getBooksByStatus(String status) async {
Future<List<Book>> getBooksByStatus(String status) => _wrap('getBooksByStatus', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
@@ -42,31 +50,29 @@ class BookDao {
whereArgs: [status, 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
});
// 根据ID获取书籍
Future<Book?> getBookById(String id) async {
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) async {
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) async {
Future<int> updateBook(Book book) => _wrap('updateBook', () async {
final db = await _dbHelper.database;
return await db.update(
'books',
@@ -74,10 +80,10 @@ class BookDao {
where: 'id = ?',
whereArgs: [book.id],
);
}
});
// 软删除书籍记录(移入回收站)
Future<int> deleteBook(String id) async {
Future<int> deleteBook(String id) => _wrap('deleteBook', () async {
final db = await _dbHelper.database;
return await db.update(
'books',
@@ -88,10 +94,10 @@ class BookDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
// 搜索书籍(标题、别名)
Future<List<Book>> searchBooks(String query) async {
Future<List<Book>> searchBooks(String query) => _wrap('searchBooks', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
@@ -99,12 +105,11 @@ class BookDao {
whereArgs: ['%$query%', '%$query%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
});
// 根据作者筛选
Future<List<Book>> getBooksByAuthor(String author) async {
Future<List<Book>> getBooksByAuthor(String author) => _wrap('getBooksByAuthor', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
@@ -112,12 +117,11 @@ class BookDao {
whereArgs: ['%$author%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
});
// 根据类型筛选
Future<List<Book>> getBooksByGenre(String genre) async {
Future<List<Book>> getBooksByGenre(String genre) => _wrap('getBooksByGenre', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
@@ -125,14 +129,13 @@ class BookDao {
whereArgs: ['%$genre%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
});
// ========== 回收站相关方法 ==========
// 获取已删除的书籍
Future<List<Book>> getDeletedBooks() async {
Future<List<Book>> getDeletedBooks() => _wrap('getDeletedBooks', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
@@ -140,12 +143,11 @@ class BookDao {
whereArgs: [1],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
});
// 恢复已删除的书籍
Future<int> restoreBook(String id) async {
Future<int> restoreBook(String id) => _wrap('restoreBook', () async {
final db = await _dbHelper.database;
return await db.update(
'books',
@@ -153,15 +155,15 @@ class BookDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
// 彻底删除书籍
Future<int> permanentDeleteBook(String id) async {
Future<int> permanentDeleteBook(String id) => _wrap('permanentDeleteBook', () async {
final db = await _dbHelper.database;
return await db.delete(
'books',
where: 'id = ?',
whereArgs: [id],
);
}
});
}

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import '../../models/data_models.dart';
import '../database_helper.dart';
@@ -6,8 +7,17 @@ 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) async {
Future<List<BookExcerpt>> getExcerptsByBookId(String bookId) => _wrap('getExcerptsByBookId', () async {
final db = await _dbHelper.database;
final maps = await db.query(
'book_excerpts',
@@ -16,24 +26,22 @@ class BookExcerptDao {
orderBy: 'created_at DESC',
);
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
}
});
/// 根据ID获取摘抄
Future<BookExcerpt?> getExcerptById(String id) async {
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);
}
if (maps.isNotEmpty) return BookExcerpt.fromJson(maps.first);
return null;
}
});
/// 插入摘抄
Future<String> insertExcerpt(BookExcerpt excerpt) async {
Future<String> insertExcerpt(BookExcerpt excerpt) => _wrap('insertExcerpt', () async {
final db = await _dbHelper.database;
await db.insert(
'book_excerpts',
@@ -41,10 +49,10 @@ class BookExcerptDao {
conflictAlgorithm: ConflictAlgorithm.replace,
);
return excerpt.id;
}
});
/// 更新摘抄
Future<void> updateExcerpt(BookExcerpt excerpt) async {
Future<void> updateExcerpt(BookExcerpt excerpt) => _wrap('updateExcerpt', () async {
final db = await _dbHelper.database;
await db.update(
'book_excerpts',
@@ -52,10 +60,10 @@ class BookExcerptDao {
where: 'id = ?',
whereArgs: [excerpt.id],
);
}
});
/// 删除摘抄(软删除)
Future<void> deleteExcerpt(String id) async {
Future<void> deleteExcerpt(String id) => _wrap('deleteExcerpt', () async {
final db = await _dbHelper.database;
await db.update(
'book_excerpts',
@@ -63,30 +71,30 @@ class BookExcerptDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
/// 彻底删除摘抄
Future<void> permanentDeleteExcerpt(String id) async {
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) async {
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() async {
Future<List<BookExcerpt>> getDeletedExcerpts() => _wrap('getDeletedExcerpts', () async {
final db = await _dbHelper.database;
final maps = await db.query(
'book_excerpts',
@@ -94,10 +102,10 @@ class BookExcerptDao {
orderBy: 'updated_at DESC',
);
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
}
});
/// 恢复已删除的摘抄
Future<void> restoreExcerpt(String id) async {
Future<void> restoreExcerpt(String id) => _wrap('restoreExcerpt', () async {
final db = await _dbHelper.database;
await db.update(
'book_excerpts',
@@ -105,5 +113,5 @@ class BookExcerptDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
}

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import '../../models/data_models.dart';
import '../database_helper.dart';
@@ -6,8 +7,17 @@ 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) async {
Future<List<BookReview>> getReviewsByBookId(String bookId) => _wrap('getReviewsByBookId', () async {
final db = await _dbHelper.database;
final maps = await db.query(
'book_reviews',
@@ -16,24 +26,22 @@ class BookReviewDao {
orderBy: 'created_at DESC',
);
return maps.map((map) => BookReview.fromJson(map)).toList();
}
});
/// 根据ID获取书评
Future<BookReview?> getReviewById(String id) async {
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);
}
if (maps.isNotEmpty) return BookReview.fromJson(maps.first);
return null;
}
});
/// 插入书评
Future<String> insertReview(BookReview review) async {
Future<String> insertReview(BookReview review) => _wrap('insertReview', () async {
final db = await _dbHelper.database;
await db.insert(
'book_reviews',
@@ -41,10 +49,10 @@ class BookReviewDao {
conflictAlgorithm: ConflictAlgorithm.replace,
);
return review.id;
}
});
/// 更新书评
Future<void> updateReview(BookReview review) async {
Future<void> updateReview(BookReview review) => _wrap('updateReview', () async {
final db = await _dbHelper.database;
await db.update(
'book_reviews',
@@ -52,10 +60,10 @@ class BookReviewDao {
where: 'id = ?',
whereArgs: [review.id],
);
}
});
/// 删除书评(软删除)
Future<void> deleteReview(String id) async {
Future<void> deleteReview(String id) => _wrap('deleteReview', () async {
final db = await _dbHelper.database;
await db.update(
'book_reviews',
@@ -63,30 +71,30 @@ class BookReviewDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
/// 彻底删除书评
Future<void> permanentDeleteReview(String id) async {
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) async {
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() async {
Future<List<BookReview>> getDeletedReviews() => _wrap('getDeletedReviews', () async {
final db = await _dbHelper.database;
final maps = await db.query(
'book_reviews',
@@ -94,10 +102,10 @@ class BookReviewDao {
orderBy: 'updated_at DESC',
);
return maps.map((map) => BookReview.fromJson(map)).toList();
}
});
/// 恢复已删除的书评
Future<void> restoreReview(String id) async {
Future<void> restoreReview(String id) => _wrap('restoreReview', () async {
final db = await _dbHelper.database;
await db.update(
'book_reviews',
@@ -105,5 +113,5 @@ class BookReviewDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
}

View File

@@ -1,5 +1,7 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'dart:io';
import 'dart:typed_data';
import '../models/data_models.dart';
/// 数据库帮助类 - 管理数据库的创建和版本控制
@@ -26,6 +28,23 @@ class DatabaseHelper {
_database = await _initDB('mooknote.db');
}
/// 从备份字节重写数据库文件并安全重连,
/// 始终按当前代码的 targetVersion 重新初始化,避免版本不一致。
Future<void> reopenDatabaseFromBytes(Uint8List bytes) async {
await close();
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'mooknote.db');
final dbFile = File(path);
if (await dbFile.exists()) {
await dbFile.delete();
}
await dbFile.writeAsBytes(bytes, flush: true);
_database = await _initDB('mooknote.db');
}
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('mooknote.db');

View File

@@ -1,9 +1,10 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
/// 图片路径管理助手 - 按分类和ID组织图片存储
///
///
/// 存储结构:
/// images/movies/{movieId}/xxxx.jpg - 影视海报
/// images/movies/{movieId}/posterimgs/xxxx.jpg - 影视海报墙图片
@@ -11,11 +12,11 @@ import 'package:path/path.dart' as p;
/// images/notes/{noteId}/xxxx.jpg - 笔记图片
class ImagePathHelper {
static final ImagePathHelper instance = ImagePathHelper._init();
ImagePathHelper._init();
String? _appDirPath;
/// 获取应用文档目录
Future<String> get _appDir async {
if (_appDirPath != null) return _appDirPath!;
@@ -23,141 +24,166 @@ class ImagePathHelper {
_appDirPath = appDir.path;
return _appDirPath!;
}
/// 获取图片根目录
Future<String> get imagesRoot async {
final appDir = await _appDir;
return p.join(appDir, 'images');
}
// ==================== 影视相关路径 ====================
/// 获取影视图片目录
/// 路径: images/movies/{movieId}/
Future<String> getMovieImagesDir(String movieId) async {
final root = await imagesRoot;
return p.join(root, 'movies', movieId);
}
/// 获取影视海报路径
/// 路径: images/movies/{movieId}/{fileName}
Future<String> getMoviePosterPath(String movieId, String fileName) async {
final dir = await getMovieImagesDir(movieId);
return p.join(dir, fileName);
}
/// 获取影视海报墙目录
/// 路径: images/movies/{movieId}/posterimgs/
Future<String> getMoviePosterImgsDir(String movieId) async {
final dir = await getMovieImagesDir(movieId);
return p.join(dir, 'posterimgs');
}
/// 获取影视海报墙图片路径
/// 路径: images/movies/{movieId}/posterimgs/{fileName}
Future<String> getMoviePosterImgPath(String movieId, String fileName) async {
final dir = await getMoviePosterImgsDir(movieId);
return p.join(dir, fileName);
}
// ==================== 书籍相关路径 ====================
/// 获取书籍图片目录
/// 路径: images/books/{bookId}/
Future<String> getBookImagesDir(String bookId) async {
final root = await imagesRoot;
return p.join(root, 'books', bookId);
}
/// 获取书籍封面路径
/// 路径: images/books/{bookId}/{fileName}
Future<String> getBookCoverPath(String bookId, String fileName) async {
final dir = await getBookImagesDir(bookId);
return p.join(dir, fileName);
}
// ==================== 笔记相关路径 ====================
/// 获取笔记图片目录
/// 路径: images/notes/{noteId}/
Future<String> getNoteImagesDir(String noteId) async {
final root = await imagesRoot;
return p.join(root, 'notes', noteId);
}
/// 获取笔记图片路径
/// 路径: images/notes/{noteId}/{fileName}
Future<String> getNoteImagePath(String noteId, String fileName) async {
final dir = await getNoteImagesDir(noteId);
return p.join(dir, fileName);
}
// ==================== 目录操作 ====================
/// 确保目录存在
Future<void> ensureDirExists(String dirPath) async {
final dir = Directory(dirPath);
if (!await dir.exists()) {
await dir.create(recursive: true);
try {
final dir = Directory(dirPath);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
} catch (e) {
debugPrint('[ImagePathHelper] ensureDirExists($dirPath) error: $e');
rethrow;
}
}
/// 删除影视图片目录(包括海报和海报墙)
/// 删除路径: images/movies/{movieId}/
Future<void> deleteMovieImages(String movieId) async {
final dirPath = await getMovieImagesDir(movieId);
await _deleteDirectory(dirPath);
}
/// 删除书籍图片目录
/// 删除路径: images/books/{bookId}/
Future<void> deleteBookImages(String bookId) async {
final dirPath = await getBookImagesDir(bookId);
await _deleteDirectory(dirPath);
}
/// 删除笔记图片目录
/// 删除路径: images/notes/{noteId}/
Future<void> deleteNoteImages(String noteId) async {
final dirPath = await getNoteImagesDir(noteId);
await _deleteDirectory(dirPath);
}
/// 删除目录及其内容
Future<void> _deleteDirectory(String dirPath) async {
final dir = Directory(dirPath);
if (await dir.exists()) {
await dir.delete(recursive: true);
try {
final dir = Directory(dirPath);
if (await dir.exists()) {
await dir.delete(recursive: true);
}
} catch (e) {
debugPrint('[ImagePathHelper] deleteDirectory($dirPath) error: $e');
rethrow;
}
}
/// 移动文件到新位置
Future<String> moveFile(String sourcePath, String targetDir, String fileName) async {
await ensureDirExists(targetDir);
final targetPath = p.join(targetDir, fileName);
final sourceFile = File(sourcePath);
if (await sourceFile.exists()) {
await sourceFile.rename(targetPath);
try {
await ensureDirExists(targetDir);
final targetPath = p.join(targetDir, fileName);
final sourceFile = File(sourcePath);
if (await sourceFile.exists()) {
await sourceFile.rename(targetPath);
}
return targetPath;
} catch (e) {
debugPrint('[ImagePathHelper] moveFile($sourcePath) error: $e');
rethrow;
}
return targetPath;
}
/// 复制文件到新位置
Future<String> copyFile(String sourcePath, String targetDir, String fileName) async {
await ensureDirExists(targetDir);
final targetPath = p.join(targetDir, fileName);
final sourceFile = File(sourcePath);
if (await sourceFile.exists()) {
await sourceFile.copy(targetPath);
try {
await ensureDirExists(targetDir);
final targetPath = p.join(targetDir, fileName);
final sourceFile = File(sourcePath);
if (await sourceFile.exists()) {
await sourceFile.copy(targetPath);
}
return targetPath;
} catch (e) {
debugPrint('[ImagePathHelper] copyFile($sourcePath) error: $e');
rethrow;
}
return targetPath;
}
/// 删除单个文件
Future<void> deleteFile(String filePath) async {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
try {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
debugPrint('[ImagePathHelper] deleteFile($filePath) error: $e');
rethrow;
}
}
}

View File

@@ -1,4 +1,4 @@
import 'package:sqflite/sqflite.dart';
import 'package:flutter/foundation.dart';
import '../../models/data_models.dart';
import '../database_helper.dart';
@@ -6,8 +6,17 @@ 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() async {
Future<List<Movie>> getAllMovies() => _wrap('getAllMovies', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'movies',
@@ -15,12 +24,11 @@ class MovieDao {
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}) async {
Future<List<Movie>> getMoviesPaged({String? status, int limit = 20, int offset = 0}) => _wrap('getMoviesPaged', () async {
final db = await _dbHelper.database;
String where = 'is_deleted = 0';
List<dynamic> whereArgs = [];
@@ -31,10 +39,10 @@ class MovieDao {
final maps = await db.query('movies', where: where, whereArgs: whereArgs,
orderBy: 'created_at DESC', limit: limit, offset: offset);
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
}
});
// 根据状态筛选影视记录
Future<List<Movie>> getMoviesByStatus(String status) async {
Future<List<Movie>> getMoviesByStatus(String status) => _wrap('getMoviesByStatus', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'movies',
@@ -42,96 +50,86 @@ class MovieDao {
whereArgs: [status, 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
}
});
// 根据导演筛选
Future<List<Movie>> getMoviesByDirector(String director) async {
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: 'is_deleted = ?',
whereArgs: [0],
where: 'directors LIKE ? AND is_deleted = ?',
whereArgs: ['%$director%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
return maps.map((m) => Movie.fromJson(m))
.where((movie) => movie.directors.contains(director))
.toList();
}
});
// 根据编剧筛选
Future<List<Movie>> getMoviesByWriter(String writer) async {
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: 'is_deleted = ?',
whereArgs: [0],
where: 'writers LIKE ? AND is_deleted = ?',
whereArgs: ['%$writer%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
return maps.map((m) => Movie.fromJson(m))
.where((movie) => movie.writers.contains(writer))
.toList();
}
});
// 根据演员筛选
Future<List<Movie>> getMoviesByActor(String actor) async {
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: 'is_deleted = ?',
whereArgs: [0],
where: 'actors LIKE ? AND is_deleted = ?',
whereArgs: ['%$actor%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
return maps.map((m) => Movie.fromJson(m))
.where((movie) => movie.actors.contains(actor))
.toList();
}
});
// 根据类型筛选
Future<List<Movie>> getMoviesByGenre(String genre) async {
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: 'is_deleted = ?',
whereArgs: [0],
where: 'genres LIKE ? AND is_deleted = ?',
whereArgs: ['%$genre%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
return maps.map((m) => Movie.fromJson(m))
.where((movie) => movie.genres.contains(genre))
.toList();
}
});
// 搜索影视(标题或别名)
Future<List<Movie>> searchMovies(String keyword) async {
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: 'is_deleted = ?',
whereArgs: [0],
where: '(title LIKE ? OR alternate_titles LIKE ?) AND is_deleted = ?',
whereArgs: [likeKeyword, likeKeyword, 0],
orderBy: 'created_at DESC',
);
final lowerKeyword = keyword.toLowerCase();
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
.where((movie) =>
movie.title.toLowerCase().contains(lowerKeyword) ||
movie.alternateTitles.any((t) => t.toLowerCase().contains(lowerKeyword)))
.toList();
}
return maps.map((m) => Movie.fromJson(m)).toList();
});
// 添加影视记录
Future<int> insertMovie(Movie movie) async {
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) async {
Future<int> updateMovie(Movie movie) => _wrap('updateMovie', () async {
final db = await _dbHelper.database;
return await db.update(
'movies',
@@ -139,10 +137,10 @@ class MovieDao {
where: 'id = ?',
whereArgs: [movie.id],
);
}
});
// 删除影视记录(软删除)
Future<int> deleteMovie(String id) async {
Future<int> deleteMovie(String id) => _wrap('deleteMovie', () async {
final db = await _dbHelper.database;
return await db.update(
'movies',
@@ -150,52 +148,73 @@ class MovieDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
// 获取所有导演(去重)
Future<List<String>> getAllDirectors() async {
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() async {
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() async {
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() async {
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() async {
Future<List<Movie>> getDeletedMovies() => _wrap('getDeletedMovies', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'movies',
@@ -203,12 +222,11 @@ class MovieDao {
whereArgs: [1],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
}
});
// 恢复已删除的影视
Future<int> restoreMovie(String id) async {
Future<int> restoreMovie(String id) => _wrap('restoreMovie', () async {
final db = await _dbHelper.database;
return await db.update(
'movies',
@@ -216,15 +234,15 @@ class MovieDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
// 彻底删除影视
Future<int> permanentDeleteMovie(String id) async {
Future<int> permanentDeleteMovie(String id) => _wrap('permanentDeleteMovie', () async {
final db = await _dbHelper.database;
return await db.delete(
'movies',
where: 'id = ?',
whereArgs: [id],
);
}
});
}

View File

@@ -1,4 +1,4 @@
import 'package:sqflite/sqflite.dart';
import 'package:flutter/foundation.dart';
import '../../models/data_models.dart';
import '../database_helper.dart';
@@ -6,8 +6,17 @@ 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) async {
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',
@@ -15,31 +24,29 @@ class MoviePosterDao {
whereArgs: [movieId],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => MoviePoster.fromJson(maps[i]));
}
});
/// 根据ID获取海报
Future<MoviePoster?> getPosterById(String id) async {
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) async {
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) async {
Future<int> deletePoster(String id) => _wrap('deletePoster', () async {
final db = await _dbHelper.database;
return await db.update(
'movie_posters',
@@ -47,15 +54,15 @@ class MoviePosterDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
/// 获取影视的海报数量
Future<int> getPosterCount(String movieId) async {
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;
}
return result.first['count'] as int? ?? 0;
});
}

View File

@@ -1,4 +1,4 @@
import 'package:sqflite/sqflite.dart';
import 'package:flutter/foundation.dart';
import '../../models/data_models.dart';
import '../database_helper.dart';
@@ -6,8 +6,17 @@ 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) async {
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',
@@ -15,31 +24,29 @@ class MovieReviewDao {
whereArgs: [movieId],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
}
});
/// 根据ID获取影评
Future<MovieReview?> getReviewById(String id) async {
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) async {
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) async {
Future<int> updateReview(MovieReview review) => _wrap('updateReview', () async {
final db = await _dbHelper.database;
return await db.update(
'movie_reviews',
@@ -47,10 +54,10 @@ class MovieReviewDao {
where: 'id = ?',
whereArgs: [review.id],
);
}
});
/// 软删除影评
Future<int> deleteReview(String id) async {
Future<int> deleteReview(String id) => _wrap('deleteReview', () async {
final db = await _dbHelper.database;
return await db.update(
'movie_reviews',
@@ -58,20 +65,20 @@ class MovieReviewDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
/// 获取影视的影评数量
Future<int> getReviewCount(String movieId) async {
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;
}
return result.first['count'] as int? ?? 0;
});
/// 获取短评列表
Future<List<MovieReview>> getShortReviews(String movieId) async {
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',
@@ -79,12 +86,11 @@ class MovieReviewDao {
whereArgs: [movieId],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
}
});
/// 获取长评列表
Future<List<MovieReview>> getLongReviews(String movieId) async {
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',
@@ -92,7 +98,6 @@ class MovieReviewDao {
whereArgs: [movieId],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
}
});
}

View File

@@ -1,4 +1,4 @@
import 'package:sqflite/sqflite.dart';
import 'package:flutter/foundation.dart';
import '../../models/data_models.dart';
import '../database_helper.dart';
@@ -6,8 +6,17 @@ 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() async {
Future<List<Note>> getAllNotes() => _wrap('getAllNotes', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'notes',
@@ -15,39 +24,37 @@ class NoteDao {
whereArgs: [0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
}
});
// 分页查询笔记
Future<List<Note>> getNotesPaged({int limit = 20, int offset = 0}) async {
Future<List<Note>> getNotesPaged({int limit = 20, int offset = 0}) => _wrap('getNotesPaged', () async {
final db = await _dbHelper.database;
final maps = await db.query('notes', where: 'is_deleted = 0',
orderBy: 'created_at DESC', limit: limit, offset: offset);
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
}
});
// 根据ID获取笔记
Future<Note?> getNoteById(String id) async {
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 = ?',
whereArgs: [id],
where: 'id = ? AND is_deleted = ?',
whereArgs: [id, 0],
);
if (maps.isEmpty) return null;
return Note.fromJson(maps.first);
}
});
// 添加笔记
Future<int> insertNote(Note note) async {
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) async {
Future<int> updateNote(Note note) => _wrap('updateNote', () async {
final db = await _dbHelper.database;
return await db.update(
'notes',
@@ -55,10 +62,10 @@ class NoteDao {
where: 'id = ?',
whereArgs: [note.id],
);
}
});
// 软删除笔记
Future<int> deleteNote(String id) async {
Future<int> deleteNote(String id) => _wrap('deleteNote', () async {
final db = await _dbHelper.database;
return await db.update(
'notes',
@@ -66,12 +73,12 @@ class NoteDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
// ========== 回收站相关方法 ==========
// 获取已删除的笔记
Future<List<Note>> getDeletedNotes() async {
Future<List<Note>> getDeletedNotes() => _wrap('getDeletedNotes', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'notes',
@@ -79,12 +86,11 @@ class NoteDao {
whereArgs: [1],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
}
});
// 恢复已删除的笔记
Future<int> restoreNote(String id) async {
Future<int> restoreNote(String id) => _wrap('restoreNote', () async {
final db = await _dbHelper.database;
return await db.update(
'notes',
@@ -92,20 +98,20 @@ class NoteDao {
where: 'id = ?',
whereArgs: [id],
);
}
});
// 彻底删除笔记
Future<int> permanentDeleteNote(String id) async {
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) async {
Future<List<Note>> searchNotes(String query) => _wrap('searchNotes', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'notes',
@@ -113,12 +119,11 @@ class NoteDao {
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) async {
Future<List<Note>> getNotesByTag(String tag) => _wrap('getNotesByTag', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'notes',
@@ -126,7 +131,6 @@ class NoteDao {
whereArgs: ['%$tag%', 0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
}
});
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
@@ -402,18 +403,8 @@ class ServerSyncService {
debugPrint('[Sync] 数据库下载响应: ${dbResp.statusCode} size=${dbResp.bodyBytes.length}');
if (dbResp.statusCode != 200) return false;
final dbPath = await DatabaseHelper.instance.databasePath;
if (dbPath != null) {
final dbFile = File(dbPath);
// 先关掉旧连接,删除旧文件,再写入新数据库
await DatabaseHelper.instance.close();
if (await dbFile.exists()) await dbFile.delete();
await dbFile.writeAsBytes(dbResp.bodyBytes);
// 设置正确的版本号
final db = await openDatabase(dbPath, version: 14);
await db.close();
debugPrint('[Sync] 数据库已写入: $dbPath');
}
await DatabaseHelper.instance.reopenDatabaseFromBytes(dbResp.bodyBytes);
debugPrint('[Sync] 数据库已重写并重新打开');
final images = (info['images'] as List<dynamic>?)
?.map((e) => e is Map ? {'name': e['name'] as String, 'rel_path': e['rel_path'] as String} : null)

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

View File

@@ -69,7 +69,7 @@ class AppTheme {
fontSize: 18,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.3,
letterSpacing: 0,
),
),
@@ -184,7 +184,7 @@ class AppTheme {
fontSize: 32,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.5,
letterSpacing: 0,
height: 1.2,
),
headlineMedium: TextStyle(
@@ -192,7 +192,7 @@ class AppTheme {
fontSize: 24,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.3,
letterSpacing: 0,
height: 1.3,
),
headlineSmall: TextStyle(
@@ -200,7 +200,7 @@ class AppTheme {
fontSize: 20,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.2,
letterSpacing: 0,
height: 1.4,
),
// 正文
@@ -282,7 +282,7 @@ class AppTheme {
fontSize: 18,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.3,
letterSpacing: 0,
),
),
@@ -380,7 +380,7 @@ class AppTheme {
fontSize: 32,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.5,
letterSpacing: 0,
height: 1.2,
),
headlineMedium: TextStyle(
@@ -388,7 +388,7 @@ class AppTheme {
fontSize: 24,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.3,
letterSpacing: 0,
height: 1.3,
),
headlineSmall: TextStyle(
@@ -396,7 +396,7 @@ class AppTheme {
fontSize: 20,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.2,
letterSpacing: 0,
height: 1.4,
),
bodyLarge: TextStyle(

View File

@@ -91,6 +91,8 @@ class UserPrefs {
// ========== 应用图标设置 ==========
// ========== Markdown 阅读器 ==========
/// Markdown 阅读器最近选择的目录
String? get lastMdFolder => prefs.getString('lastMdFolder');
Future<bool> setLastMdFolder(String value) => prefs.setString('lastMdFolder', value);