Files
MookNote/lib/providers/app_provider.dart
2026-07-02 05:22:21 +08:00

719 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:collection';
import 'package:flutter/material.dart';
import '../models/data_models.dart';
import '../models/note_plus_models.dart';
import '../utils/movie/movie_dao.dart';
import '../utils/book/book_dao.dart';
import '../utils/note/note_dao.dart';
import '../utils/movie/movie_review_dao.dart';
import '../utils/movie/movie_poster_dao.dart';
import '../utils/book/book_review_dao.dart';
import '../utils/book/book_excerpt_dao.dart';
import '../utils/note_plus/note_plus_dao.dart';
import '../utils/tag/tag_dao.dart';
import '../utils/database_helper.dart';
import '../utils/image_path_helper.dart';
import '../utils/user_prefs.dart';
import '../utils/theme/app_theme.dart';
/// 应用全局状态管理
class AppProvider extends ChangeNotifier {
// 数据库访问对象
final MovieDao _movieDao = MovieDao();
final BookDao _bookDao = BookDao();
final NoteDao _noteDao = NoteDao();
final MovieReviewDao _reviewDao = MovieReviewDao();
final MoviePosterDao _posterDao = MoviePosterDao();
final BookReviewDao _bookReviewDao = BookReviewDao();
final BookExcerptDao _bookExcerptDao = BookExcerptDao();
final NotePlusDao _notePlusDao = NotePlusDao();
final TagDao _tagDao = TagDao();
// 数据列表
List<Movie> _movies = [];
List<Book> _books = [];
List<Note> _notes = [];
// 当前主界面选中的标签 (0: 观影1: 阅读2: 笔记)
int _mainTabIndex = 0;
// 当前底部导航选中的索引 (0: 主页1: 新增2: 我的)
int _bottomNavIndex = 0;
// 底部导航栏是否可见
bool _bottomNavVisible = true;
// 平板 Master-Detail 选中项
Movie? _selectedMovie;
Book? _selectedBook;
Note? _selectedNote;
// 主题模式
ThemeMode _themeMode = ThemeMode.system;
// 配色方案索引
int _colorSchemeIndex = 0;
// 字体
String _fontFamily = '';
// 观影选中的状态 (0: 已看1: 想看2: 在看)
int _movieStatusIndex = 0;
// 影视列表布局样式 (0: 网格, 1: 列表, 2: 大图卡片)
int _movieLayoutStyle = 0;
// 阅读选中的状态 (0: 读完1: 在读2: 准备读)
int _bookStatusIndex = 0;
// 侧边菜单是否打开
bool _drawerOpen = false;
// 回到顶部信号(点击首页图标时递增)
int _scrollToTopSignal = 0;
int get scrollToTopSignal => _scrollToTopSignal;
// 编辑后刷新信号(影视/书籍编辑返回时递增)
int _editRefreshCounter = 0;
int get editRefreshCounter => _editRefreshCounter;
// 初始化数据库
Future<void> initDatabase() async {
debugPrint('[AppProvider] initDatabase');
final results = await Future.wait([
_movieDao.getAllMovies(),
_bookDao.getAllBooks(),
_noteDao.getAllNotes(),
]);
_movies = results[0] as List<Movie>;
_books = results[1] as List<Book>;
_notes = results[2] as List<Note>;
debugPrint('[AppProvider] 本地数据: movies=${_movies.length}, books=${_books.length}, notes=${_notes.length}');
notifyListeners();
}
// 从用户偏好恢复默认启动标签
void initMainTabIndex() {
final userPrefs = UserPrefs();
_movieLayoutStyle = userPrefs.movieLayoutStyle;
final defaultIndex = userPrefs.defaultMainTabIndex;
// 确保选中的标签是启用的
final showMovie = userPrefs.showMovieTab;
final showBook = userPrefs.showBookTab;
final showNote = userPrefs.showNoteTab;
final enabled = [showMovie, showBook, showNote];
if (defaultIndex >= 0 && defaultIndex < enabled.length && enabled[defaultIndex]) {
_mainTabIndex = defaultIndex;
} else {
// 回退到第一个启用的标签
if (showMovie) {
_mainTabIndex = 0;
} else if (showBook) {
_mainTabIndex = 1;
} else {
_mainTabIndex = 2;
}
}
notifyListeners();
}
// 加载影视数据
Future<void> loadMovies() async {
_movies = await _movieDao.getAllMovies();
notifyListeners();
}
// 加载书籍数据
Future<void> loadBooks() async {
_books = await _bookDao.getAllBooks();
notifyListeners();
}
// 加载笔记数据
Future<void> loadNotes({int sortMode = 0}) async {
_notes = await _noteDao.getAllNotes(sortMode: sortMode);
notifyListeners();
}
/// 编辑返回后触发列表页重载
void setEditRefresh() {
_editRefreshCounter++;
notifyListeners();
}
// ─── 分页加载(供列表页触底加载使用)────────────────────────
static const int _pageSize = 20;
Future<List<Movie>> loadMoviesPaged({String? status, required int offset, int sortMode = 0}) async {
return _movieDao.getMoviesPaged(status: status, limit: _pageSize, offset: offset, sortMode: sortMode);
}
Future<List<Book>> loadBooksPaged({String? status, required int offset, int sortMode = 0}) async {
return _bookDao.getBooksPaged(status: status, limit: _pageSize, offset: offset, sortMode: sortMode);
}
Future<List<Note>> loadNotesPaged({required int offset, int sortMode = 0}) async {
return _noteDao.getNotesPaged(limit: _pageSize, offset: offset, sortMode: sortMode);
}
// Getters
int get mainTabIndex => _mainTabIndex;
int get bottomNavIndex => _bottomNavIndex;
Movie? get selectedMovie => _selectedMovie;
Book? get selectedBook => _selectedBook;
Note? get selectedNote => _selectedNote;
int get movieStatusIndex => _movieStatusIndex;
int get movieLayoutStyle => _movieLayoutStyle;
int get bookStatusIndex => _bookStatusIndex;
bool get drawerOpen => _drawerOpen;
bool get bottomNavVisible => _bottomNavVisible;
ThemeMode get themeMode => _themeMode;
int get colorSchemeIndex => _colorSchemeIndex;
String get fontFamily => _fontFamily;
List<Movie> get movies => UnmodifiableListView(_movies);
List<Book> get books => UnmodifiableListView(_books);
List<Note> get notes => UnmodifiableListView(_notes);
// 根据状态获取影视列表
List<Movie> getMoviesByStatus(String status) {
return _movies.where((movie) => movie.status == status).toList();
}
// 根据状态获取书籍列表
List<Book> getBooksByStatus(String status) {
return _books.where((book) => book.status == status).toList();
}
// Setters
void setMainTabIndex(int index) {
_mainTabIndex = index;
notifyListeners();
}
void setBottomNavIndex(int index) {
if (index == 0 && _bottomNavIndex == 0) {
// 已在首页,再次点击 → 回到顶部
_scrollToTopSignal++;
notifyListeners();
return;
}
_bottomNavIndex = index;
_bottomNavVisible = true;
notifyListeners();
}
void selectMovie(Movie? movie) {
_selectedMovie = movie;
notifyListeners();
}
void selectBook(Book? book) {
_selectedBook = book;
notifyListeners();
}
void selectNote(Note? note) {
_selectedNote = note;
notifyListeners();
}
void setBottomNavVisible(bool visible) {
if (_bottomNavVisible != visible) {
_bottomNavVisible = visible;
notifyListeners();
}
}
void setThemeMode(ThemeMode mode) {
if (_themeMode != mode) {
_themeMode = mode;
UserPrefs().setThemeMode(mode.index); // 0=system, 1=light, 2=dark
notifyListeners();
}
}
void loadThemeMode() {
final prefs = UserPrefs();
switch (prefs.themeMode) {
case 1:
_themeMode = ThemeMode.light;
case 2:
_themeMode = ThemeMode.dark;
default:
_themeMode = ThemeMode.system;
}
_colorSchemeIndex = prefs.colorSchemeIndex;
_fontFamily = prefs.fontFamily;
AppTheme.setFontFamily(_fontFamily);
notifyListeners();
}
void setColorScheme(int index) {
if (_colorSchemeIndex != index) {
_colorSchemeIndex = index;
UserPrefs().setColorSchemeIndex(index);
notifyListeners();
}
}
void setFontFamily(String family) {
if (_fontFamily != family) {
_fontFamily = family;
UserPrefs().setFontFamily(family);
AppTheme.setFontFamily(family);
notifyListeners();
}
}
void setMovieStatusIndex(int index) {
_movieStatusIndex = index;
notifyListeners();
}
void setMovieLayoutStyle(int style) {
_movieLayoutStyle = style;
UserPrefs().setMovieLayoutStyle(style);
notifyListeners();
}
void setBookStatusIndex(int index) {
_bookStatusIndex = index;
notifyListeners();
}
void toggleDrawer() {
_drawerOpen = !_drawerOpen;
notifyListeners();
}
void closeDrawer() {
_drawerOpen = false;
notifyListeners();
}
// ─── 图片上传辅助 ────────────────────────────────────────────────
// 添加影视记录
Future<void> addMovie(Movie movie) async {
await _movieDao.insertMovie(movie);
await loadMovies();
}
Future<void> updateMovie(Movie movie) async {
await _movieDao.updateMovie(movie);
await loadMovies();
}
/// 仅更新封面偏移量(不触发全量刷新)
Future<void> updateMovieCoverOffset(String movieId, double offset) async {
await _movieDao.updateCoverOffset(movieId, offset);
final idx = _movies.indexWhere((m) => m.id == movieId);
if (idx != -1) {
_movies[idx] = _movies[idx].copyWith(coverOffset: offset);
notifyListeners();
}
}
/// 仅更新封面偏移量(不触发全量刷新)
Future<void> updateBookCoverOffset(String bookId, double offset) async {
await _bookDao.updateCoverOffset(bookId, offset);
final idx = _books.indexWhere((b) => b.id == bookId);
if (idx != -1) {
_books[idx] = _books[idx].copyWith(coverOffset: offset);
notifyListeners();
}
}
Future<void> removeMovie(String id) async {
await _movieDao.deleteMovie(id);
await loadMovies();
}
Future<void> addBook(Book book) async {
await _bookDao.insertBook(book);
await loadBooks();
}
Future<void> updateBook(Book book) async {
await _bookDao.updateBook(book);
await loadBooks();
}
Future<void> removeBook(String id) async {
await _bookDao.deleteBook(id);
await loadBooks();
}
Future<void> addNote(Note note) async {
await _noteDao.insertNote(note);
await loadNotes();
}
Future<void> updateNote(Note note) async {
await _noteDao.updateNote(note);
await loadNotes();
}
Future<void> removeNote(String id) async {
await _noteDao.deleteNote(id);
await loadNotes();
}
Future<void> toggleNotePin(String id, bool isPinned) async {
await _noteDao.togglePin(id, isPinned);
await loadNotes();
notifyListeners();
}
// ========== 影评相关方法 ==========
/// 获取影视的所有影评
Future<List<MovieReview>> getMovieReviews(String movieId) async {
return await _reviewDao.getReviewsByMovieId(movieId);
}
/// 添加影评
Future<void> addMovieReview(MovieReview review) async {
await _reviewDao.insertReview(review);
}
/// 更新影评
Future<void> updateMovieReview(MovieReview review) async {
await _reviewDao.updateReview(review);
}
/// 删除影评
Future<void> removeMovieReview(String id) async {
await _reviewDao.deleteReview(id);
}
/// 获取影视的影评数量
Future<int> getMovieReviewCount(String movieId) async {
return await _reviewDao.getReviewCount(movieId);
}
// ========== 海报墙相关方法 ==========
/// 获取影视的所有海报
Future<List<MoviePoster>> getMoviePosters(String movieId) async {
return await _posterDao.getPostersByMovieId(movieId);
}
/// 添加海报
Future<void> addMoviePoster(MoviePoster poster) async {
await _posterDao.insertPoster(poster);
}
/// 删除海报
Future<void> removeMoviePoster(String id) async {
final poster = await _posterDao.getPosterById(id);
if (poster != null) {
await ImagePathHelper.instance.deleteFile(poster.posterPath);
}
await _posterDao.deletePoster(id);
}
/// 获取影视的海报数量
Future<int> getMoviePosterCount(String movieId) async {
return await _posterDao.getPosterCount(movieId);
}
// ========== 书评相关方法 ==========
/// 获取书籍的所有书评
Future<List<BookReview>> getBookReviews(String bookId) async {
return await _bookReviewDao.getReviewsByBookId(bookId);
}
/// 添加书评
Future<void> addBookReview(BookReview review) async {
await _bookReviewDao.insertReview(review);
}
/// 更新书评
Future<void> updateBookReview(BookReview review) async {
await _bookReviewDao.updateReview(review);
}
/// 删除书评
Future<void> removeBookReview(String id) async {
await _bookReviewDao.deleteReview(id);
}
/// 获取书籍的书评数量
Future<int> getBookReviewCount(String bookId) async {
return await _bookReviewDao.getReviewCount(bookId);
}
// ========== 摘抄相关方法 ==========
/// 获取书籍的所有摘抄
Future<List<BookExcerpt>> getBookExcerpts(String bookId) async {
return await _bookExcerptDao.getExcerptsByBookId(bookId);
}
/// 添加摘抄
Future<void> addBookExcerpt(BookExcerpt excerpt) async {
await _bookExcerptDao.insertExcerpt(excerpt);
}
/// 更新摘抄
Future<void> updateBookExcerpt(BookExcerpt excerpt) async {
await _bookExcerptDao.updateExcerpt(excerpt);
}
/// 删除摘抄
Future<void> removeBookExcerpt(String id) async {
await _bookExcerptDao.deleteExcerpt(id);
}
/// 获取书籍的摘抄数量
Future<int> getBookExcerptCount(String bookId) async {
return await _bookExcerptDao.getExcerptCount(bookId);
}
// ========== 回收站相关方法 ==========
/// 获取已删除的影视
Future<List<Movie>> getDeletedMovies() async {
return await _movieDao.getDeletedMovies();
}
/// 恢复影视
Future<void> restoreMovie(String id) async {
await _movieDao.restoreMovie(id);
await loadMovies();
}
/// 彻底删除影视
Future<void> permanentDeleteMovie(String id) async {
await ImagePathHelper.instance.deleteMovieImages(id);
await _movieDao.permanentDeleteMovie(id);
}
/// 获取已删除的书籍
Future<List<Book>> getDeletedBooks() async {
return await _bookDao.getDeletedBooks();
}
/// 恢复书籍
Future<void> restoreBook(String id) async {
await _bookDao.restoreBook(id);
await loadBooks();
}
/// 彻底删除书籍
Future<void> permanentDeleteBook(String id) async {
await ImagePathHelper.instance.deleteBookImages(id);
await _bookDao.permanentDeleteBook(id);
}
/// 获取已删除的笔记
Future<List<Note>> getDeletedNotes() async {
return await _noteDao.getDeletedNotes();
}
/// 恢复笔记
Future<void> restoreNote(String id) async {
await _noteDao.restoreNote(id);
await loadNotes();
}
/// 彻底删除笔记
Future<void> permanentDeleteNote(String id) async {
await ImagePathHelper.instance.deleteNoteImages(id);
await _noteDao.permanentDeleteNote(id);
}
/// 清空回收站
Future<void> clearRecycleBin() async {
final deletedMovies = await getDeletedMovies();
final deletedBooks = await getDeletedBooks();
final deletedNotes = await getDeletedNotes();
final deletedMovieReviews = await getDeletedMovieReviews();
final deletedBookReviews = await getDeletedBookReviews();
final deletedBookExcerpts = await getDeletedBookExcerpts();
final deletedNotePlusDocs = await _notePlusDao.getDeleted();
for (final movie in deletedMovies) {
await permanentDeleteMovie(movie.id);
}
for (final book in deletedBooks) {
await permanentDeleteBook(book.id);
}
for (final note in deletedNotes) {
await permanentDeleteNote(note.id);
}
for (final review in deletedMovieReviews) {
await _reviewDao.permanentDeleteReview(review.id);
}
for (final review in deletedBookReviews) {
await _bookReviewDao.permanentDeleteReview(review.id);
}
for (final excerpt in deletedBookExcerpts) {
await _bookExcerptDao.permanentDeleteExcerpt(excerpt.id);
}
for (final doc in deletedNotePlusDocs) {
await _notePlusDao.permanentDelete(doc.id);
}
await loadMovies();
await loadBooks();
await loadNotes();
}
// ========== 影评书评回收站 ==========
Future<List<MovieReview>> getDeletedMovieReviews() async {
return await _reviewDao.getDeletedReviews();
}
Future<void> restoreMovieReview(String id) async {
await _reviewDao.restoreReview(id);
}
Future<void> permanentDeleteMovieReview(String id) async {
await _reviewDao.permanentDeleteReview(id);
}
Future<List<BookReview>> getDeletedBookReviews() async {
return await _bookReviewDao.getDeletedReviews();
}
Future<void> restoreBookReview(String id) async {
await _bookReviewDao.restoreReview(id);
}
Future<void> permanentDeleteBookReview(String id) async {
await _bookReviewDao.permanentDeleteReview(id);
}
// ========== 摘抄回收站方法 ==========
Future<List<BookExcerpt>> getDeletedBookExcerpts() async {
return await _bookExcerptDao.getDeletedExcerpts();
}
Future<void> restoreBookExcerpt(String id) async {
await _bookExcerptDao.restoreExcerpt(id);
}
Future<void> permanentDeleteBookExcerpt(String id) async {
await _bookExcerptDao.permanentDeleteExcerpt(id);
}
// ========== Note Plus 回收站 ==========
Future<List<NotePlusDocument>> getDeletedNotePlusDocs() async {
return await _notePlusDao.getDeleted();
}
Future<void> restoreNotePlusDoc(String id) async {
await _notePlusDao.restore(id);
}
Future<void> permanentDeleteNotePlusDoc(String id) async {
await _notePlusDao.permanentDelete(id);
}
// ========== 标签管理方法 ==========
Future<List<Map<String, dynamic>>> getTags(String type, {bool excludeHidden = false}) async {
return await _tagDao.getTagsByType(type, excludeHidden: excludeHidden);
}
Future<void> toggleTagHidden(String tagId) async {
await _tagDao.toggleHidden(tagId);
notifyListeners();
}
Future<String> addTag(String name, String type) async {
final id = await _tagDao.addTag(name, type);
await _reloadByTagType(type);
return id;
}
Future<bool> renameTag(String tagId, String newName, String type) async {
final result = await _tagDao.renameTag(tagId, newName);
if (result) {
await _reloadByTagType(type);
}
return result;
}
Future<void> deleteTag(String tagId, String type,
{String? replacementName}) async {
await _tagDao.deleteTag(tagId, replacementName: replacementName);
await _reloadByTagType(type);
}
/// 仅删除标签本身,不级联影响已有条目
Future<void> deleteTagOnly(String tagId, String type) async {
await _tagDao.deleteTagOnly(tagId);
await _reloadByTagType(type);
}
/// 从影视/书籍/笔记数据中解析标签,同步到 tags 表
Future<int> syncTagsFromData() async {
final db = await DatabaseHelper.instance.database;
final now = DateTime.now().toIso8601String();
int counter = 0;
int added = 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,
});
added++;
} 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');
}
}
return added;
}
Future<void> _reloadByTagType(String type) async {
switch (type) {
case 'movie_genre':
await loadMovies();
case 'book_genre':
await loadBooks();
case 'note_tag':
await loadNotes();
}
}
}