generated from dellevin/template
优化
This commit is contained in:
144
AGENTS.md
Normal file
144
AGENTS.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
||||||
|
|
||||||
|
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||||
|
|
||||||
|
## 1. Think Before Coding
|
||||||
|
|
||||||
|
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||||
|
|
||||||
|
Before implementing:
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them - don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
## 2. Simplicity First
|
||||||
|
|
||||||
|
**Minimum code that solves the problem. Nothing speculative.**
|
||||||
|
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
|
||||||
|
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||||
|
|
||||||
|
## 3. Surgical Changes
|
||||||
|
|
||||||
|
**Touch only what you must. Clean up only your own mess.**
|
||||||
|
|
||||||
|
When editing existing code:
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- Match existing style, even if you'd do it differently.
|
||||||
|
- If you notice unrelated dead code, mention it - don't delete it.
|
||||||
|
|
||||||
|
When your changes create orphans:
|
||||||
|
- Remove imports/variables/functions that YOUR changes made unused.
|
||||||
|
- Don't remove pre-existing dead code unless asked.
|
||||||
|
|
||||||
|
The test: Every changed line should trace directly to the user's request.
|
||||||
|
|
||||||
|
## 4. Goal-Driven Execution
|
||||||
|
|
||||||
|
**Define success criteria. Loop until verified.**
|
||||||
|
|
||||||
|
Transform tasks into verifiable goals:
|
||||||
|
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||||
|
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||||
|
- "Refactor X" → "Ensure tests pass before and after"
|
||||||
|
|
||||||
|
For multi-step tasks, state a brief plan:
|
||||||
|
```
|
||||||
|
1. [Step] → verify: [check]
|
||||||
|
2. [Step] → verify: [check]
|
||||||
|
3. [Step] → verify: [check]
|
||||||
|
```
|
||||||
|
|
||||||
|
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
MookNote is a Flutter-based Android app for tracking movies, books, and notes. Language: Dart/Flutter with Chinese UI. AGPL-3.0 licensed.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# Run the app (connect Android device or start emulator first)
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# Build release APK
|
||||||
|
flutter build apk --release
|
||||||
|
|
||||||
|
# Build App Bundle (for Google Play)
|
||||||
|
flutter build appbundle --release
|
||||||
|
|
||||||
|
# Clean build
|
||||||
|
flutter clean && flutter pub get
|
||||||
|
|
||||||
|
# Static analysis
|
||||||
|
flutter analyze
|
||||||
|
|
||||||
|
# Use China mirror if pub get fails
|
||||||
|
$env:PUB_HOSTED_URL="https://pub.flutter-io.cn"
|
||||||
|
$env:FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### App Structure (lib/)
|
||||||
|
|
||||||
|
- **main.dart** — App entry; initializes `UserPrefs`, `AppProvider`, auto-backup, usage stats, and sync validation on startup
|
||||||
|
- **models/data_models.dart** — All data models in one file: `Movie`, `Book`, `Note`, `MovieReview`, `BookReview`, `MoviePoster`, `BookExcerpt`. Each has `fromJson`/`toJson`/`copyWith`
|
||||||
|
- **providers/app_provider.dart** — Single `AppProvider` (ChangeNotifier) holds all app state. Manages movies, books, notes lists, theme mode, tab indices, drawer state. Uses DAO pattern for data access. Has a `_useRemote` flag to switch between local SQLite and remote server
|
||||||
|
- **pages/** — UI pages organized by feature domain: `movies/`, `book/`, `note/`, `sync/`, `markdown_reader/`
|
||||||
|
- **utils/** — Business logic layer:
|
||||||
|
- `database_helper.dart` — SQLite database (sqflite), version 13, with migration chain
|
||||||
|
- `movie/`, `book/`, `note/` — DAO classes for each entity (CRUD operations)
|
||||||
|
- `tag/tag_dao.dart` — Tag management
|
||||||
|
- `sync/` — Server sync, WebDAV sync, auto-backup, backup service
|
||||||
|
- `theme/app_theme.dart` — Minimalist black/white/gray theme with Material 3
|
||||||
|
- `user_prefs.dart` — SharedPreferences wrapper (singleton)
|
||||||
|
- **widgets/** — Shared reusable widgets (list items, star rating, drawer, bottom nav, shimmer skeleton)
|
||||||
|
- **utils/app_router.dart** — Named route generator using `onGenerateRoute` with `SlideUpPageRoute` transitions
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
Provider pattern. A single `AppProvider` (ChangeNotifier) is provided at the root via `MultiProvider`/`Consumer`. All pages read state from `context.watch<AppProvider>()` or `context.read<AppProvider>()`.
|
||||||
|
|
||||||
|
### Data Layer
|
||||||
|
|
||||||
|
- **Local**: SQLite via sqflite (`DatabaseHelper` singleton, DB name: `mooknote.db`)
|
||||||
|
- **Remote**: Optional server sync via `ServerSyncService` / `ServerDataService`. Controlled by `UserPrefs.syncEnabled` + activation code
|
||||||
|
- **DAO pattern**: Each entity has its own DAO (`MovieDao`, `BookDao`, `NoteDao`, etc.) that can operate locally or remotely based on `_useRemote` flag in AppProvider
|
||||||
|
- **Soft delete**: All models have `isDeleted` field; a recycle bin page manages restores
|
||||||
|
|
||||||
|
### Image Storage
|
||||||
|
|
||||||
|
Images stored at: `/mooknote/images/<category>/<entity_id>/<filename>` on device. `ImagePathHelper` manages paths. `FadeInLocalImage` widget handles local image display.
|
||||||
|
|
||||||
|
### Server (server/)
|
||||||
|
|
||||||
|
Python Flask backend for user stats and sync API. Not part of the Flutter build. Run with `python app.py` or gunicorn.
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- All Chinese UI strings are hardcoded (no i18n framework beyond Flutter's built-in localization delegates)
|
||||||
|
- UUID-based IDs for all entities (generated at creation time)
|
||||||
|
- List fields (directors, actors, genres, tags) stored as JSON-encoded strings in SQLite, parsed via `Movie.parseStringList()`
|
||||||
|
- `copyWith` uses a `_CopyWithNullSentinel` pattern to distinguish "not passed" from "passed null" for nullable fields
|
||||||
|
- Route transitions use custom `SlideUpPageRoute` (bottom-to-top slide)
|
||||||
|
- DB migrations in `DatabaseHelper._onUpgrade` — always add new migration blocks with version checks (`if (oldVersion < N)`)
|
||||||
@@ -233,6 +233,7 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
if (widget.review == null) {
|
if (widget.review == null) {
|
||||||
@@ -264,5 +265,9 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
|||||||
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
|
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ToastUtil.show(context, '保存失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
int _lastDataCount = -1;
|
int _lastDataCount = -1;
|
||||||
DateTime? _lastUpdatedAt;
|
DateTime? _lastUpdatedAt;
|
||||||
late ScrollController _scrollController;
|
late ScrollController _scrollController;
|
||||||
|
AppProvider? _provider;
|
||||||
|
|
||||||
static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
_scrollController = ScrollController()..addListener(_onScroll);
|
_scrollController = ScrollController()..addListener(_onScroll);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
|
_provider = provider;
|
||||||
provider.addListener(_onDataChanged);
|
provider.addListener(_onDataChanged);
|
||||||
_lastDataCount = provider.books.length;
|
_lastDataCount = provider.books.length;
|
||||||
if (provider.books.isNotEmpty) _lastUpdatedAt = provider.books.first.updatedAt;
|
if (provider.books.isNotEmpty) _lastUpdatedAt = provider.books.first.updatedAt;
|
||||||
@@ -47,6 +49,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_provider?.removeListener(_onDataChanged);
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1806,6 +1806,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// 收集所有多值字段输入框中的未提交内容
|
// 收集所有多值字段输入框中的未提交内容
|
||||||
_collectUnsubmittedValues();
|
_collectUnsubmittedValues();
|
||||||
|
|
||||||
@@ -1869,6 +1870,10 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
|||||||
ToastUtil.show(context, widget.movie == null ? '添加成功' : '更新成功');
|
ToastUtil.show(context, widget.movie == null ? '添加成功' : '更新成功');
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ToastUtil.show(context, '保存失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将海报从临时ID目录移动到新的影视ID目录
|
/// 将海报从临时ID目录移动到新的影视ID目录
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
Future<void> _saveReview() async {
|
Future<void> _saveReview() async {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
if (widget.review == null) {
|
if (widget.review == null) {
|
||||||
@@ -331,5 +332,9 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
|
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ToastUtil.show(context, '保存失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
int _lastDataCount = -1;
|
int _lastDataCount = -1;
|
||||||
DateTime? _lastUpdatedAt;
|
DateTime? _lastUpdatedAt;
|
||||||
late ScrollController _scrollController;
|
late ScrollController _scrollController;
|
||||||
|
AppProvider? _provider;
|
||||||
|
|
||||||
static const _statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
|
static const _statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
_scrollController = ScrollController()..addListener(_onScroll);
|
_scrollController = ScrollController()..addListener(_onScroll);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
|
_provider = provider;
|
||||||
provider.addListener(_onDataChanged);
|
provider.addListener(_onDataChanged);
|
||||||
_lastDataCount = provider.movies.length;
|
_lastDataCount = provider.movies.length;
|
||||||
if (provider.movies.isNotEmpty) _lastUpdatedAt = provider.movies.first.updatedAt;
|
if (provider.movies.isNotEmpty) _lastUpdatedAt = provider.movies.first.updatedAt;
|
||||||
@@ -47,6 +49,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_provider?.removeListener(_onDataChanged);
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -825,6 +825,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
if (_isEditing) {
|
if (_isEditing) {
|
||||||
@@ -873,6 +874,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ToastUtil.show(context, '保存失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将图片从临时ID目录移动到新ID目录
|
/// 将图片从临时ID目录移动到新ID目录
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
int _offset = 0;
|
int _offset = 0;
|
||||||
late ScrollController _scrollController;
|
late ScrollController _scrollController;
|
||||||
|
AppProvider? _provider;
|
||||||
int _layoutStyle = 0;
|
int _layoutStyle = 0;
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
int _lastDataCount = -1;
|
int _lastDataCount = -1;
|
||||||
@@ -33,6 +34,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
_scrollController = ScrollController()..addListener(_onScroll);
|
_scrollController = ScrollController()..addListener(_onScroll);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final provider = context.read<AppProvider>();
|
final provider = context.read<AppProvider>();
|
||||||
|
_provider = provider;
|
||||||
provider.addListener(_onDataChanged);
|
provider.addListener(_onDataChanged);
|
||||||
_lastDataCount = provider.notes.length;
|
_lastDataCount = provider.notes.length;
|
||||||
if (provider.notes.isNotEmpty) _lastUpdatedAt = provider.notes.first.updatedAt;
|
if (provider.notes.isNotEmpty) _lastUpdatedAt = provider.notes.first.updatedAt;
|
||||||
@@ -42,6 +44,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_provider?.removeListener(_onDataChanged);
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../models/data_models.dart';
|
import '../models/data_models.dart';
|
||||||
|
import 'slide_up_page_route.dart';
|
||||||
import '../pages/movies/movie_form_page.dart';
|
import '../pages/movies/movie_form_page.dart';
|
||||||
import '../pages/book/book_form_page.dart';
|
import '../pages/book/book_form_page.dart';
|
||||||
import '../pages/note/note_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/book/book_detail_page.dart';
|
||||||
import '../pages/note/note_detail_page.dart';
|
import '../pages/note/note_detail_page.dart';
|
||||||
import '../pages/movies/douban_webview_page.dart';
|
import '../pages/movies/douban_webview_page.dart';
|
||||||
import 'slide_up_page_route.dart';
|
|
||||||
|
|
||||||
/// 路由生成器
|
/// 路由生成器
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
@@ -15,56 +15,65 @@ class AppRouter {
|
|||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
case '/movie-form':
|
case '/movie-form':
|
||||||
final args = settings.arguments;
|
final args = settings.arguments;
|
||||||
Movie? movie;
|
final Movie? movie = args is Movie ? args : null;
|
||||||
String? initialStatus;
|
final String? initialStatus =
|
||||||
if (args is Movie) {
|
args is Map<String, dynamic> ? (args['initialStatus'] as String?) : null;
|
||||||
movie = args;
|
|
||||||
} else if (args is Map<String, dynamic>) {
|
|
||||||
initialStatus = args['initialStatus'] as String?;
|
|
||||||
}
|
|
||||||
return SlideUpPageRoute(
|
return SlideUpPageRoute(
|
||||||
page: MovieFormPage(movie: movie, initialStatus: initialStatus),
|
page: MovieFormPage(movie: movie, initialStatus: initialStatus),
|
||||||
);
|
);
|
||||||
|
|
||||||
case '/book-form':
|
case '/book-form':
|
||||||
final args = settings.arguments;
|
final args = settings.arguments;
|
||||||
Book? book;
|
final Book? book = args is Book ? args : null;
|
||||||
String? initialStatus;
|
final String? initialStatus =
|
||||||
if (args is Book) {
|
args is Map<String, dynamic> ? (args['initialStatus'] as String?) : null;
|
||||||
book = args;
|
|
||||||
} else if (args is Map<String, dynamic>) {
|
|
||||||
initialStatus = args['initialStatus'] as String?;
|
|
||||||
}
|
|
||||||
return SlideUpPageRoute(
|
return SlideUpPageRoute(
|
||||||
page: BookFormPage(book: book, initialStatus: initialStatus),
|
page: BookFormPage(book: book, initialStatus: initialStatus),
|
||||||
);
|
);
|
||||||
|
|
||||||
case '/note-form':
|
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));
|
return SlideUpPageRoute(page: NoteFormPage(note: note));
|
||||||
|
|
||||||
case '/movie-detail':
|
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));
|
return SlideUpPageRoute(page: MovieDetailPage(movie: movie));
|
||||||
|
|
||||||
case '/book-detail':
|
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));
|
return SlideUpPageRoute(page: BookDetailPage(book: book));
|
||||||
|
|
||||||
case '/note-detail':
|
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));
|
return SlideUpPageRoute(page: NoteDetailPage(note: note));
|
||||||
|
|
||||||
case '/douban-webview':
|
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));
|
return SlideUpPageRoute(page: DoubanWebViewPage(url: url));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return MaterialPageRoute(
|
return _buildUnknownRoute(settings.name);
|
||||||
builder: (_) => Scaffold(
|
|
||||||
body: Center(child: Text('未找到页面:${settings.name}')),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Route<dynamic> _buildUnknownRoute(String? name) {
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
body: Center(child: Text('未找到页面:${name ?? ''}')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
|
|
||||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
|||||||
class BookDao {
|
class BookDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'books',
|
'books',
|
||||||
@@ -15,12 +24,11 @@ class BookDao {
|
|||||||
whereArgs: [0],
|
whereArgs: [0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
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;
|
final db = await _dbHelper.database;
|
||||||
String where = 'is_deleted = 0';
|
String where = 'is_deleted = 0';
|
||||||
List<dynamic> whereArgs = [];
|
List<dynamic> whereArgs = [];
|
||||||
@@ -31,10 +39,10 @@ class BookDao {
|
|||||||
final maps = await db.query('books', where: where, whereArgs: whereArgs,
|
final maps = await db.query('books', where: where, whereArgs: whereArgs,
|
||||||
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
||||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'books',
|
'books',
|
||||||
@@ -42,31 +50,29 @@ class BookDao {
|
|||||||
whereArgs: [status, 0],
|
whereArgs: [status, 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||||
}
|
});
|
||||||
|
|
||||||
// 根据ID获取书籍
|
// 根据ID获取书籍
|
||||||
Future<Book?> getBookById(String id) async {
|
Future<Book?> getBookById(String id) => _wrap('getBookById', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'books',
|
'books',
|
||||||
where: 'id = ? AND is_deleted = ?',
|
where: 'id = ? AND is_deleted = ?',
|
||||||
whereArgs: [id, 0],
|
whereArgs: [id, 0],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (maps.isEmpty) return null;
|
if (maps.isEmpty) return null;
|
||||||
return Book.fromJson(maps.first);
|
return Book.fromJson(maps.first);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 添加书籍记录
|
// 添加书籍记录
|
||||||
Future<int> insertBook(Book book) async {
|
Future<int> insertBook(Book book) => _wrap('insertBook', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.insert('books', book.toJson());
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'books',
|
'books',
|
||||||
@@ -74,10 +80,10 @@ class BookDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [book.id],
|
whereArgs: [book.id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 软删除书籍记录(移入回收站)
|
// 软删除书籍记录(移入回收站)
|
||||||
Future<int> deleteBook(String id) async {
|
Future<int> deleteBook(String id) => _wrap('deleteBook', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'books',
|
'books',
|
||||||
@@ -88,10 +94,10 @@ class BookDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'books',
|
'books',
|
||||||
@@ -99,12 +105,11 @@ class BookDao {
|
|||||||
whereArgs: ['%$query%', '%$query%', 0],
|
whereArgs: ['%$query%', '%$query%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'books',
|
'books',
|
||||||
@@ -112,12 +117,11 @@ class BookDao {
|
|||||||
whereArgs: ['%$author%', 0],
|
whereArgs: ['%$author%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'books',
|
'books',
|
||||||
@@ -125,14 +129,13 @@ class BookDao {
|
|||||||
whereArgs: ['%$genre%', 0],
|
whereArgs: ['%$genre%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'books',
|
'books',
|
||||||
@@ -140,12 +143,11 @@ class BookDao {
|
|||||||
whereArgs: [1],
|
whereArgs: [1],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'books',
|
'books',
|
||||||
@@ -153,15 +155,15 @@ class BookDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 彻底删除书籍
|
// 彻底删除书籍
|
||||||
Future<int> permanentDeleteBook(String id) async {
|
Future<int> permanentDeleteBook(String id) => _wrap('permanentDeleteBook', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.delete(
|
return await db.delete(
|
||||||
'books',
|
'books',
|
||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
@@ -6,8 +7,17 @@ import '../database_helper.dart';
|
|||||||
class BookExcerptDao {
|
class BookExcerptDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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 db = await _dbHelper.database;
|
||||||
final maps = await db.query(
|
final maps = await db.query(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
@@ -16,24 +26,22 @@ class BookExcerptDao {
|
|||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
|
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 根据ID获取摘抄
|
/// 根据ID获取摘抄
|
||||||
Future<BookExcerpt?> getExcerptById(String id) async {
|
Future<BookExcerpt?> getExcerptById(String id) => _wrap('getExcerptById', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final maps = await db.query(
|
final maps = await db.query(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
where: 'id = ? AND is_deleted = 0',
|
where: 'id = ? AND is_deleted = 0',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
if (maps.isNotEmpty) {
|
if (maps.isNotEmpty) return BookExcerpt.fromJson(maps.first);
|
||||||
return BookExcerpt.fromJson(maps.first);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 插入摘抄
|
/// 插入摘抄
|
||||||
Future<String> insertExcerpt(BookExcerpt excerpt) async {
|
Future<String> insertExcerpt(BookExcerpt excerpt) => _wrap('insertExcerpt', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.insert(
|
await db.insert(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
@@ -41,10 +49,10 @@ class BookExcerptDao {
|
|||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
);
|
);
|
||||||
return excerpt.id;
|
return excerpt.id;
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 更新摘抄
|
/// 更新摘抄
|
||||||
Future<void> updateExcerpt(BookExcerpt excerpt) async {
|
Future<void> updateExcerpt(BookExcerpt excerpt) => _wrap('updateExcerpt', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.update(
|
await db.update(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
@@ -52,10 +60,10 @@ class BookExcerptDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [excerpt.id],
|
whereArgs: [excerpt.id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 删除摘抄(软删除)
|
/// 删除摘抄(软删除)
|
||||||
Future<void> deleteExcerpt(String id) async {
|
Future<void> deleteExcerpt(String id) => _wrap('deleteExcerpt', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.update(
|
await db.update(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
@@ -63,30 +71,30 @@ class BookExcerptDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 彻底删除摘抄
|
/// 彻底删除摘抄
|
||||||
Future<void> permanentDeleteExcerpt(String id) async {
|
Future<void> permanentDeleteExcerpt(String id) => _wrap('permanentDeleteExcerpt', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.delete(
|
await db.delete(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 获取摘抄数量
|
/// 获取摘抄数量
|
||||||
Future<int> getExcerptCount(String bookId) async {
|
Future<int> getExcerptCount(String bookId) => _wrap('getExcerptCount', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final result = await db.rawQuery(
|
final result = await db.rawQuery(
|
||||||
'SELECT COUNT(*) as count FROM book_excerpts WHERE book_id = ? AND is_deleted = 0',
|
'SELECT COUNT(*) as count FROM book_excerpts WHERE book_id = ? AND is_deleted = 0',
|
||||||
[bookId],
|
[bookId],
|
||||||
);
|
);
|
||||||
return result.first['count'] as int? ?? 0;
|
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 db = await _dbHelper.database;
|
||||||
final maps = await db.query(
|
final maps = await db.query(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
@@ -94,10 +102,10 @@ class BookExcerptDao {
|
|||||||
orderBy: 'updated_at DESC',
|
orderBy: 'updated_at DESC',
|
||||||
);
|
);
|
||||||
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
|
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;
|
final db = await _dbHelper.database;
|
||||||
await db.update(
|
await db.update(
|
||||||
'book_excerpts',
|
'book_excerpts',
|
||||||
@@ -105,5 +113,5 @@ class BookExcerptDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
@@ -6,8 +7,17 @@ import '../database_helper.dart';
|
|||||||
class BookReviewDao {
|
class BookReviewDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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 db = await _dbHelper.database;
|
||||||
final maps = await db.query(
|
final maps = await db.query(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
@@ -16,24 +26,22 @@ class BookReviewDao {
|
|||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
return maps.map((map) => BookReview.fromJson(map)).toList();
|
return maps.map((map) => BookReview.fromJson(map)).toList();
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 根据ID获取书评
|
/// 根据ID获取书评
|
||||||
Future<BookReview?> getReviewById(String id) async {
|
Future<BookReview?> getReviewById(String id) => _wrap('getReviewById', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final maps = await db.query(
|
final maps = await db.query(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
where: 'id = ? AND is_deleted = 0',
|
where: 'id = ? AND is_deleted = 0',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
if (maps.isNotEmpty) {
|
if (maps.isNotEmpty) return BookReview.fromJson(maps.first);
|
||||||
return BookReview.fromJson(maps.first);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 插入书评
|
/// 插入书评
|
||||||
Future<String> insertReview(BookReview review) async {
|
Future<String> insertReview(BookReview review) => _wrap('insertReview', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.insert(
|
await db.insert(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
@@ -41,10 +49,10 @@ class BookReviewDao {
|
|||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
);
|
);
|
||||||
return review.id;
|
return review.id;
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 更新书评
|
/// 更新书评
|
||||||
Future<void> updateReview(BookReview review) async {
|
Future<void> updateReview(BookReview review) => _wrap('updateReview', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.update(
|
await db.update(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
@@ -52,10 +60,10 @@ class BookReviewDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [review.id],
|
whereArgs: [review.id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 删除书评(软删除)
|
/// 删除书评(软删除)
|
||||||
Future<void> deleteReview(String id) async {
|
Future<void> deleteReview(String id) => _wrap('deleteReview', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.update(
|
await db.update(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
@@ -63,30 +71,30 @@ class BookReviewDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 彻底删除书评
|
/// 彻底删除书评
|
||||||
Future<void> permanentDeleteReview(String id) async {
|
Future<void> permanentDeleteReview(String id) => _wrap('permanentDeleteReview', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
await db.delete(
|
await db.delete(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 获取书评数量
|
/// 获取书评数量
|
||||||
Future<int> getReviewCount(String bookId) async {
|
Future<int> getReviewCount(String bookId) => _wrap('getReviewCount', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final result = await db.rawQuery(
|
final result = await db.rawQuery(
|
||||||
'SELECT COUNT(*) as count FROM book_reviews WHERE book_id = ? AND is_deleted = 0',
|
'SELECT COUNT(*) as count FROM book_reviews WHERE book_id = ? AND is_deleted = 0',
|
||||||
[bookId],
|
[bookId],
|
||||||
);
|
);
|
||||||
return result.first['count'] as int? ?? 0;
|
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 db = await _dbHelper.database;
|
||||||
final maps = await db.query(
|
final maps = await db.query(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
@@ -94,10 +102,10 @@ class BookReviewDao {
|
|||||||
orderBy: 'updated_at DESC',
|
orderBy: 'updated_at DESC',
|
||||||
);
|
);
|
||||||
return maps.map((map) => BookReview.fromJson(map)).toList();
|
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;
|
final db = await _dbHelper.database;
|
||||||
await db.update(
|
await db.update(
|
||||||
'book_reviews',
|
'book_reviews',
|
||||||
@@ -105,5 +113,5 @@ class BookReviewDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
import '../models/data_models.dart';
|
import '../models/data_models.dart';
|
||||||
|
|
||||||
/// 数据库帮助类 - 管理数据库的创建和版本控制
|
/// 数据库帮助类 - 管理数据库的创建和版本控制
|
||||||
@@ -26,6 +28,23 @@ class DatabaseHelper {
|
|||||||
_database = await _initDB('mooknote.db');
|
_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 {
|
Future<Database> get database async {
|
||||||
if (_database != null) return _database!;
|
if (_database != null) return _database!;
|
||||||
_database = await _initDB('mooknote.db');
|
_database = await _initDB('mooknote.db');
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
/// 图片路径管理助手 - 按分类和ID组织图片存储
|
/// 图片路径管理助手 - 按分类和ID组织图片存储
|
||||||
///
|
///
|
||||||
/// 存储结构:
|
/// 存储结构:
|
||||||
/// images/movies/{movieId}/xxxx.jpg - 影视海报
|
/// images/movies/{movieId}/xxxx.jpg - 影视海报
|
||||||
/// images/movies/{movieId}/posterimgs/xxxx.jpg - 影视海报墙图片
|
/// images/movies/{movieId}/posterimgs/xxxx.jpg - 影视海报墙图片
|
||||||
@@ -11,11 +12,11 @@ import 'package:path/path.dart' as p;
|
|||||||
/// images/notes/{noteId}/xxxx.jpg - 笔记图片
|
/// images/notes/{noteId}/xxxx.jpg - 笔记图片
|
||||||
class ImagePathHelper {
|
class ImagePathHelper {
|
||||||
static final ImagePathHelper instance = ImagePathHelper._init();
|
static final ImagePathHelper instance = ImagePathHelper._init();
|
||||||
|
|
||||||
ImagePathHelper._init();
|
ImagePathHelper._init();
|
||||||
|
|
||||||
String? _appDirPath;
|
String? _appDirPath;
|
||||||
|
|
||||||
/// 获取应用文档目录
|
/// 获取应用文档目录
|
||||||
Future<String> get _appDir async {
|
Future<String> get _appDir async {
|
||||||
if (_appDirPath != null) return _appDirPath!;
|
if (_appDirPath != null) return _appDirPath!;
|
||||||
@@ -23,141 +24,166 @@ class ImagePathHelper {
|
|||||||
_appDirPath = appDir.path;
|
_appDirPath = appDir.path;
|
||||||
return _appDirPath!;
|
return _appDirPath!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取图片根目录
|
/// 获取图片根目录
|
||||||
Future<String> get imagesRoot async {
|
Future<String> get imagesRoot async {
|
||||||
final appDir = await _appDir;
|
final appDir = await _appDir;
|
||||||
return p.join(appDir, 'images');
|
return p.join(appDir, 'images');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 影视相关路径 ====================
|
// ==================== 影视相关路径 ====================
|
||||||
|
|
||||||
/// 获取影视图片目录
|
/// 获取影视图片目录
|
||||||
/// 路径: images/movies/{movieId}/
|
/// 路径: images/movies/{movieId}/
|
||||||
Future<String> getMovieImagesDir(String movieId) async {
|
Future<String> getMovieImagesDir(String movieId) async {
|
||||||
final root = await imagesRoot;
|
final root = await imagesRoot;
|
||||||
return p.join(root, 'movies', movieId);
|
return p.join(root, 'movies', movieId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取影视海报路径
|
/// 获取影视海报路径
|
||||||
/// 路径: images/movies/{movieId}/{fileName}
|
/// 路径: images/movies/{movieId}/{fileName}
|
||||||
Future<String> getMoviePosterPath(String movieId, String fileName) async {
|
Future<String> getMoviePosterPath(String movieId, String fileName) async {
|
||||||
final dir = await getMovieImagesDir(movieId);
|
final dir = await getMovieImagesDir(movieId);
|
||||||
return p.join(dir, fileName);
|
return p.join(dir, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取影视海报墙目录
|
/// 获取影视海报墙目录
|
||||||
/// 路径: images/movies/{movieId}/posterimgs/
|
/// 路径: images/movies/{movieId}/posterimgs/
|
||||||
Future<String> getMoviePosterImgsDir(String movieId) async {
|
Future<String> getMoviePosterImgsDir(String movieId) async {
|
||||||
final dir = await getMovieImagesDir(movieId);
|
final dir = await getMovieImagesDir(movieId);
|
||||||
return p.join(dir, 'posterimgs');
|
return p.join(dir, 'posterimgs');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取影视海报墙图片路径
|
/// 获取影视海报墙图片路径
|
||||||
/// 路径: images/movies/{movieId}/posterimgs/{fileName}
|
/// 路径: images/movies/{movieId}/posterimgs/{fileName}
|
||||||
Future<String> getMoviePosterImgPath(String movieId, String fileName) async {
|
Future<String> getMoviePosterImgPath(String movieId, String fileName) async {
|
||||||
final dir = await getMoviePosterImgsDir(movieId);
|
final dir = await getMoviePosterImgsDir(movieId);
|
||||||
return p.join(dir, fileName);
|
return p.join(dir, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 书籍相关路径 ====================
|
// ==================== 书籍相关路径 ====================
|
||||||
|
|
||||||
/// 获取书籍图片目录
|
/// 获取书籍图片目录
|
||||||
/// 路径: images/books/{bookId}/
|
/// 路径: images/books/{bookId}/
|
||||||
Future<String> getBookImagesDir(String bookId) async {
|
Future<String> getBookImagesDir(String bookId) async {
|
||||||
final root = await imagesRoot;
|
final root = await imagesRoot;
|
||||||
return p.join(root, 'books', bookId);
|
return p.join(root, 'books', bookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取书籍封面路径
|
/// 获取书籍封面路径
|
||||||
/// 路径: images/books/{bookId}/{fileName}
|
/// 路径: images/books/{bookId}/{fileName}
|
||||||
Future<String> getBookCoverPath(String bookId, String fileName) async {
|
Future<String> getBookCoverPath(String bookId, String fileName) async {
|
||||||
final dir = await getBookImagesDir(bookId);
|
final dir = await getBookImagesDir(bookId);
|
||||||
return p.join(dir, fileName);
|
return p.join(dir, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 笔记相关路径 ====================
|
// ==================== 笔记相关路径 ====================
|
||||||
|
|
||||||
/// 获取笔记图片目录
|
/// 获取笔记图片目录
|
||||||
/// 路径: images/notes/{noteId}/
|
/// 路径: images/notes/{noteId}/
|
||||||
Future<String> getNoteImagesDir(String noteId) async {
|
Future<String> getNoteImagesDir(String noteId) async {
|
||||||
final root = await imagesRoot;
|
final root = await imagesRoot;
|
||||||
return p.join(root, 'notes', noteId);
|
return p.join(root, 'notes', noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取笔记图片路径
|
/// 获取笔记图片路径
|
||||||
/// 路径: images/notes/{noteId}/{fileName}
|
/// 路径: images/notes/{noteId}/{fileName}
|
||||||
Future<String> getNoteImagePath(String noteId, String fileName) async {
|
Future<String> getNoteImagePath(String noteId, String fileName) async {
|
||||||
final dir = await getNoteImagesDir(noteId);
|
final dir = await getNoteImagesDir(noteId);
|
||||||
return p.join(dir, fileName);
|
return p.join(dir, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 目录操作 ====================
|
// ==================== 目录操作 ====================
|
||||||
|
|
||||||
/// 确保目录存在
|
/// 确保目录存在
|
||||||
Future<void> ensureDirExists(String dirPath) async {
|
Future<void> ensureDirExists(String dirPath) async {
|
||||||
final dir = Directory(dirPath);
|
try {
|
||||||
if (!await dir.exists()) {
|
final dir = Directory(dirPath);
|
||||||
await dir.create(recursive: true);
|
if (!await dir.exists()) {
|
||||||
|
await dir.create(recursive: true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[ImagePathHelper] ensureDirExists($dirPath) error: $e');
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除影视图片目录(包括海报和海报墙)
|
/// 删除影视图片目录(包括海报和海报墙)
|
||||||
/// 删除路径: images/movies/{movieId}/
|
/// 删除路径: images/movies/{movieId}/
|
||||||
Future<void> deleteMovieImages(String movieId) async {
|
Future<void> deleteMovieImages(String movieId) async {
|
||||||
final dirPath = await getMovieImagesDir(movieId);
|
final dirPath = await getMovieImagesDir(movieId);
|
||||||
await _deleteDirectory(dirPath);
|
await _deleteDirectory(dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除书籍图片目录
|
/// 删除书籍图片目录
|
||||||
/// 删除路径: images/books/{bookId}/
|
/// 删除路径: images/books/{bookId}/
|
||||||
Future<void> deleteBookImages(String bookId) async {
|
Future<void> deleteBookImages(String bookId) async {
|
||||||
final dirPath = await getBookImagesDir(bookId);
|
final dirPath = await getBookImagesDir(bookId);
|
||||||
await _deleteDirectory(dirPath);
|
await _deleteDirectory(dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除笔记图片目录
|
/// 删除笔记图片目录
|
||||||
/// 删除路径: images/notes/{noteId}/
|
/// 删除路径: images/notes/{noteId}/
|
||||||
Future<void> deleteNoteImages(String noteId) async {
|
Future<void> deleteNoteImages(String noteId) async {
|
||||||
final dirPath = await getNoteImagesDir(noteId);
|
final dirPath = await getNoteImagesDir(noteId);
|
||||||
await _deleteDirectory(dirPath);
|
await _deleteDirectory(dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除目录及其内容
|
/// 删除目录及其内容
|
||||||
Future<void> _deleteDirectory(String dirPath) async {
|
Future<void> _deleteDirectory(String dirPath) async {
|
||||||
final dir = Directory(dirPath);
|
try {
|
||||||
if (await dir.exists()) {
|
final dir = Directory(dirPath);
|
||||||
await dir.delete(recursive: true);
|
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 {
|
Future<String> moveFile(String sourcePath, String targetDir, String fileName) async {
|
||||||
await ensureDirExists(targetDir);
|
try {
|
||||||
final targetPath = p.join(targetDir, fileName);
|
await ensureDirExists(targetDir);
|
||||||
final sourceFile = File(sourcePath);
|
final targetPath = p.join(targetDir, fileName);
|
||||||
if (await sourceFile.exists()) {
|
final sourceFile = File(sourcePath);
|
||||||
await sourceFile.rename(targetPath);
|
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 {
|
Future<String> copyFile(String sourcePath, String targetDir, String fileName) async {
|
||||||
await ensureDirExists(targetDir);
|
try {
|
||||||
final targetPath = p.join(targetDir, fileName);
|
await ensureDirExists(targetDir);
|
||||||
final sourceFile = File(sourcePath);
|
final targetPath = p.join(targetDir, fileName);
|
||||||
if (await sourceFile.exists()) {
|
final sourceFile = File(sourcePath);
|
||||||
await sourceFile.copy(targetPath);
|
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 {
|
Future<void> deleteFile(String filePath) async {
|
||||||
final file = File(filePath);
|
try {
|
||||||
if (await file.exists()) {
|
final file = File(filePath);
|
||||||
await file.delete();
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[ImagePathHelper] deleteFile($filePath) error: $e');
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
|
|
||||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
|||||||
class MovieDao {
|
class MovieDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
@@ -15,12 +24,11 @@ class MovieDao {
|
|||||||
whereArgs: [0],
|
whereArgs: [0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
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;
|
final db = await _dbHelper.database;
|
||||||
String where = 'is_deleted = 0';
|
String where = 'is_deleted = 0';
|
||||||
List<dynamic> whereArgs = [];
|
List<dynamic> whereArgs = [];
|
||||||
@@ -31,10 +39,10 @@ class MovieDao {
|
|||||||
final maps = await db.query('movies', where: where, whereArgs: whereArgs,
|
final maps = await db.query('movies', where: where, whereArgs: whereArgs,
|
||||||
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
@@ -42,96 +50,86 @@ class MovieDao {
|
|||||||
whereArgs: [status, 0],
|
whereArgs: [status, 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
where: 'is_deleted = ?',
|
where: 'directors LIKE ? AND is_deleted = ?',
|
||||||
whereArgs: [0],
|
whereArgs: ['%$director%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
return maps.map((m) => Movie.fromJson(m))
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
|
||||||
.where((movie) => movie.directors.contains(director))
|
.where((movie) => movie.directors.contains(director))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
});
|
||||||
|
|
||||||
// 根据编剧筛选
|
// 根据编剧筛选
|
||||||
Future<List<Movie>> getMoviesByWriter(String writer) async {
|
Future<List<Movie>> getMoviesByWriter(String writer) => _wrap('getMoviesByWriter', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
where: 'is_deleted = ?',
|
where: 'writers LIKE ? AND is_deleted = ?',
|
||||||
whereArgs: [0],
|
whereArgs: ['%$writer%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
return maps.map((m) => Movie.fromJson(m))
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
|
||||||
.where((movie) => movie.writers.contains(writer))
|
.where((movie) => movie.writers.contains(writer))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
});
|
||||||
|
|
||||||
// 根据演员筛选
|
// 根据演员筛选
|
||||||
Future<List<Movie>> getMoviesByActor(String actor) async {
|
Future<List<Movie>> getMoviesByActor(String actor) => _wrap('getMoviesByActor', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
where: 'is_deleted = ?',
|
where: 'actors LIKE ? AND is_deleted = ?',
|
||||||
whereArgs: [0],
|
whereArgs: ['%$actor%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
return maps.map((m) => Movie.fromJson(m))
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
|
||||||
.where((movie) => movie.actors.contains(actor))
|
.where((movie) => movie.actors.contains(actor))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
});
|
||||||
|
|
||||||
// 根据类型筛选
|
// 根据类型筛选
|
||||||
Future<List<Movie>> getMoviesByGenre(String genre) async {
|
Future<List<Movie>> getMoviesByGenre(String genre) => _wrap('getMoviesByGenre', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
where: 'is_deleted = ?',
|
where: 'genres LIKE ? AND is_deleted = ?',
|
||||||
whereArgs: [0],
|
whereArgs: ['%$genre%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
return maps.map((m) => Movie.fromJson(m))
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
|
||||||
.where((movie) => movie.genres.contains(genre))
|
.where((movie) => movie.genres.contains(genre))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
});
|
||||||
|
|
||||||
// 搜索影视(标题或别名)
|
// 搜索影视(标题或别名)
|
||||||
Future<List<Movie>> searchMovies(String keyword) async {
|
Future<List<Movie>> searchMovies(String keyword) => _wrap('searchMovies', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
|
final likeKeyword = '%$keyword%';
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
where: 'is_deleted = ?',
|
where: '(title LIKE ? OR alternate_titles LIKE ?) AND is_deleted = ?',
|
||||||
whereArgs: [0],
|
whereArgs: [likeKeyword, likeKeyword, 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
return maps.map((m) => Movie.fromJson(m)).toList();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加影视记录
|
// 添加影视记录
|
||||||
Future<int> insertMovie(Movie movie) async {
|
Future<int> insertMovie(Movie movie) => _wrap('insertMovie', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.insert('movies', movie.toJson());
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'movies',
|
'movies',
|
||||||
@@ -139,10 +137,10 @@ class MovieDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [movie.id],
|
whereArgs: [movie.id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 删除影视记录(软删除)
|
// 删除影视记录(软删除)
|
||||||
Future<int> deleteMovie(String id) async {
|
Future<int> deleteMovie(String id) => _wrap('deleteMovie', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'movies',
|
'movies',
|
||||||
@@ -150,52 +148,73 @@ class MovieDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 获取所有导演(去重)
|
// 获取所有导演(去重)
|
||||||
Future<List<String>> getAllDirectors() async {
|
Future<List<String>> getAllDirectors() => _wrap('getAllDirectors', () async {
|
||||||
final movies = await getAllMovies();
|
final movies = await getAllMovies();
|
||||||
final directors = <String>{};
|
final directors = <String>{};
|
||||||
for (final movie in movies) {
|
for (final movie in movies) {
|
||||||
directors.addAll(movie.directors);
|
directors.addAll(movie.directors);
|
||||||
}
|
}
|
||||||
return directors.toList()..sort();
|
return directors.toList()..sort();
|
||||||
}
|
});
|
||||||
|
|
||||||
// 获取所有编剧(去重)
|
// 获取所有编剧(去重)
|
||||||
Future<List<String>> getAllWriters() async {
|
Future<List<String>> getAllWriters() => _wrap('getAllWriters', () async {
|
||||||
final movies = await getAllMovies();
|
final movies = await getAllMovies();
|
||||||
final writers = <String>{};
|
final writers = <String>{};
|
||||||
for (final movie in movies) {
|
for (final movie in movies) {
|
||||||
writers.addAll(movie.writers);
|
writers.addAll(movie.writers);
|
||||||
}
|
}
|
||||||
return writers.toList()..sort();
|
return writers.toList()..sort();
|
||||||
}
|
});
|
||||||
|
|
||||||
// 获取所有演员(去重)
|
// 获取所有演员(去重)
|
||||||
Future<List<String>> getAllActors() async {
|
Future<List<String>> getAllActors() => _wrap('getAllActors', () async {
|
||||||
final movies = await getAllMovies();
|
final movies = await getAllMovies();
|
||||||
final actors = <String>{};
|
final actors = <String>{};
|
||||||
for (final movie in movies) {
|
for (final movie in movies) {
|
||||||
actors.addAll(movie.actors);
|
actors.addAll(movie.actors);
|
||||||
}
|
}
|
||||||
return actors.toList()..sort();
|
return actors.toList()..sort();
|
||||||
}
|
});
|
||||||
|
|
||||||
// 获取所有类型(去重)
|
// 获取所有类型(去重)
|
||||||
Future<List<String>> getAllGenres() async {
|
Future<List<String>> getAllGenres() => _wrap('getAllGenres', () async {
|
||||||
final movies = await getAllMovies();
|
final movies = await getAllMovies();
|
||||||
final genres = <String>{};
|
final genres = <String>{};
|
||||||
for (final movie in movies) {
|
for (final movie in movies) {
|
||||||
genres.addAll(movie.genres);
|
genres.addAll(movie.genres);
|
||||||
}
|
}
|
||||||
return genres.toList()..sort();
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movies',
|
'movies',
|
||||||
@@ -203,12 +222,11 @@ class MovieDao {
|
|||||||
whereArgs: [1],
|
whereArgs: [1],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'movies',
|
'movies',
|
||||||
@@ -216,15 +234,15 @@ class MovieDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 彻底删除影视
|
// 彻底删除影视
|
||||||
Future<int> permanentDeleteMovie(String id) async {
|
Future<int> permanentDeleteMovie(String id) => _wrap('permanentDeleteMovie', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.delete(
|
return await db.delete(
|
||||||
'movies',
|
'movies',
|
||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
|
|
||||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
|||||||
class MoviePosterDao {
|
class MoviePosterDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movie_posters',
|
'movie_posters',
|
||||||
@@ -15,31 +24,29 @@ class MoviePosterDao {
|
|||||||
whereArgs: [movieId],
|
whereArgs: [movieId],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => MoviePoster.fromJson(maps[i]));
|
return List.generate(maps.length, (i) => MoviePoster.fromJson(maps[i]));
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 根据ID获取海报
|
/// 根据ID获取海报
|
||||||
Future<MoviePoster?> getPosterById(String id) async {
|
Future<MoviePoster?> getPosterById(String id) => _wrap('getPosterById', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movie_posters',
|
'movie_posters',
|
||||||
where: 'id = ? AND is_deleted = 0',
|
where: 'id = ? AND is_deleted = 0',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (maps.isEmpty) return null;
|
if (maps.isEmpty) return null;
|
||||||
return MoviePoster.fromJson(maps.first);
|
return MoviePoster.fromJson(maps.first);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 添加海报
|
/// 添加海报
|
||||||
Future<int> insertPoster(MoviePoster poster) async {
|
Future<int> insertPoster(MoviePoster poster) => _wrap('insertPoster', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.insert('movie_posters', poster.toJson());
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'movie_posters',
|
'movie_posters',
|
||||||
@@ -47,15 +54,15 @@ class MoviePosterDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 获取影视的海报数量
|
/// 获取影视的海报数量
|
||||||
Future<int> getPosterCount(String movieId) async {
|
Future<int> getPosterCount(String movieId) => _wrap('getPosterCount', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final result = await db.rawQuery(
|
final result = await db.rawQuery(
|
||||||
'SELECT COUNT(*) as count FROM movie_posters WHERE movie_id = ? AND is_deleted = 0',
|
'SELECT COUNT(*) as count FROM movie_posters WHERE movie_id = ? AND is_deleted = 0',
|
||||||
[movieId],
|
[movieId],
|
||||||
);
|
);
|
||||||
return result.first['count'] as int;
|
return result.first['count'] as int? ?? 0;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
|
|
||||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
|||||||
class MovieReviewDao {
|
class MovieReviewDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movie_reviews',
|
'movie_reviews',
|
||||||
@@ -15,31 +24,29 @@ class MovieReviewDao {
|
|||||||
whereArgs: [movieId],
|
whereArgs: [movieId],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 根据ID获取影评
|
/// 根据ID获取影评
|
||||||
Future<MovieReview?> getReviewById(String id) async {
|
Future<MovieReview?> getReviewById(String id) => _wrap('getReviewById', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movie_reviews',
|
'movie_reviews',
|
||||||
where: 'id = ? AND is_deleted = 0',
|
where: 'id = ? AND is_deleted = 0',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (maps.isEmpty) return null;
|
if (maps.isEmpty) return null;
|
||||||
return MovieReview.fromJson(maps.first);
|
return MovieReview.fromJson(maps.first);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 添加影评
|
/// 添加影评
|
||||||
Future<int> insertReview(MovieReview review) async {
|
Future<int> insertReview(MovieReview review) => _wrap('insertReview', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.insert('movie_reviews', review.toJson());
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'movie_reviews',
|
'movie_reviews',
|
||||||
@@ -47,10 +54,10 @@ class MovieReviewDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [review.id],
|
whereArgs: [review.id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 软删除影评
|
/// 软删除影评
|
||||||
Future<int> deleteReview(String id) async {
|
Future<int> deleteReview(String id) => _wrap('deleteReview', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'movie_reviews',
|
'movie_reviews',
|
||||||
@@ -58,20 +65,20 @@ class MovieReviewDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 获取影视的影评数量
|
/// 获取影视的影评数量
|
||||||
Future<int> getReviewCount(String movieId) async {
|
Future<int> getReviewCount(String movieId) => _wrap('getReviewCount', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final result = await db.rawQuery(
|
final result = await db.rawQuery(
|
||||||
'SELECT COUNT(*) as count FROM movie_reviews WHERE movie_id = ? AND is_deleted = 0',
|
'SELECT COUNT(*) as count FROM movie_reviews WHERE movie_id = ? AND is_deleted = 0',
|
||||||
[movieId],
|
[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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movie_reviews',
|
'movie_reviews',
|
||||||
@@ -79,12 +86,11 @@ class MovieReviewDao {
|
|||||||
whereArgs: [movieId],
|
whereArgs: [movieId],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'movie_reviews',
|
'movie_reviews',
|
||||||
@@ -92,7 +98,6 @@ class MovieReviewDao {
|
|||||||
whereArgs: [movieId],
|
whereArgs: [movieId],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
|
|
||||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
|||||||
class NoteDao {
|
class NoteDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'notes',
|
'notes',
|
||||||
@@ -15,39 +24,37 @@ class NoteDao {
|
|||||||
whereArgs: [0],
|
whereArgs: [0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final maps = await db.query('notes', where: 'is_deleted = 0',
|
final maps = await db.query('notes', where: 'is_deleted = 0',
|
||||||
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
||||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||||
}
|
});
|
||||||
|
|
||||||
// 根据ID获取笔记
|
// 根据ID获取笔记
|
||||||
Future<Note?> getNoteById(String id) async {
|
Future<Note?> getNoteById(String id) => _wrap('getNoteById', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'notes',
|
'notes',
|
||||||
where: 'id = ?',
|
where: 'id = ? AND is_deleted = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id, 0],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (maps.isEmpty) return null;
|
if (maps.isEmpty) return null;
|
||||||
return Note.fromJson(maps.first);
|
return Note.fromJson(maps.first);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 添加笔记
|
// 添加笔记
|
||||||
Future<int> insertNote(Note note) async {
|
Future<int> insertNote(Note note) => _wrap('insertNote', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.insert('notes', note.toJson());
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'notes',
|
'notes',
|
||||||
@@ -55,10 +62,10 @@ class NoteDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [note.id],
|
whereArgs: [note.id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 软删除笔记
|
// 软删除笔记
|
||||||
Future<int> deleteNote(String id) async {
|
Future<int> deleteNote(String id) => _wrap('deleteNote', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'notes',
|
'notes',
|
||||||
@@ -66,12 +73,12 @@ class NoteDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ========== 回收站相关方法 ==========
|
// ========== 回收站相关方法 ==========
|
||||||
|
|
||||||
// 获取已删除的笔记
|
// 获取已删除的笔记
|
||||||
Future<List<Note>> getDeletedNotes() async {
|
Future<List<Note>> getDeletedNotes() => _wrap('getDeletedNotes', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'notes',
|
'notes',
|
||||||
@@ -79,12 +86,11 @@ class NoteDao {
|
|||||||
whereArgs: [1],
|
whereArgs: [1],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'notes',
|
'notes',
|
||||||
@@ -92,20 +98,20 @@ class NoteDao {
|
|||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 彻底删除笔记
|
// 彻底删除笔记
|
||||||
Future<int> permanentDeleteNote(String id) async {
|
Future<int> permanentDeleteNote(String id) => _wrap('permanentDeleteNote', () async {
|
||||||
final db = await _dbHelper.database;
|
final db = await _dbHelper.database;
|
||||||
return await db.delete(
|
return await db.delete(
|
||||||
'notes',
|
'notes',
|
||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'notes',
|
'notes',
|
||||||
@@ -113,12 +119,11 @@ class NoteDao {
|
|||||||
whereArgs: ['%$query%', '%$query%', '%$query%', 0],
|
whereArgs: ['%$query%', '%$query%', '%$query%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
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 db = await _dbHelper.database;
|
||||||
final List<Map<String, dynamic>> maps = await db.query(
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
'notes',
|
'notes',
|
||||||
@@ -126,7 +131,6 @@ class NoteDao {
|
|||||||
whereArgs: ['%$tag%', 0],
|
whereArgs: ['%$tag%', 0],
|
||||||
orderBy: 'created_at DESC',
|
orderBy: 'created_at DESC',
|
||||||
);
|
);
|
||||||
|
|
||||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -402,18 +403,8 @@ class ServerSyncService {
|
|||||||
debugPrint('[Sync] 数据库下载响应: ${dbResp.statusCode} size=${dbResp.bodyBytes.length}');
|
debugPrint('[Sync] 数据库下载响应: ${dbResp.statusCode} size=${dbResp.bodyBytes.length}');
|
||||||
if (dbResp.statusCode != 200) return false;
|
if (dbResp.statusCode != 200) return false;
|
||||||
|
|
||||||
final dbPath = await DatabaseHelper.instance.databasePath;
|
await DatabaseHelper.instance.reopenDatabaseFromBytes(dbResp.bodyBytes);
|
||||||
if (dbPath != null) {
|
debugPrint('[Sync] 数据库已重写并重新打开');
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
final images = (info['images'] as List<dynamic>?)
|
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)
|
?.map((e) => e is Map ? {'name': e['name'] as String, 'rel_path': e['rel_path'] as String} : null)
|
||||||
|
|||||||
@@ -1,27 +1,38 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
import '../database_helper.dart';
|
import '../database_helper.dart';
|
||||||
|
|
||||||
class TagDao {
|
class TagDao {
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
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;
|
final db = await _dbHelper.database;
|
||||||
return await db.query('tags',
|
return await db.query('tags',
|
||||||
where: 'type = ?',
|
where: 'type = ?',
|
||||||
whereArgs: [type],
|
whereArgs: [type],
|
||||||
orderBy: 'name ASC');
|
orderBy: 'name ASC');
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 根据ID获取标签
|
/// 根据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 db = await _dbHelper.database;
|
||||||
final results = await db.query('tags', where: 'id = ?', whereArgs: [id]);
|
final results = await db.query('tags', where: 'id = ?', whereArgs: [id]);
|
||||||
return results.isNotEmpty ? results.first : null;
|
return results.isNotEmpty ? results.first : null;
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 添加标签,返回新标签ID
|
/// 添加标签,返回新标签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 db = await _dbHelper.database;
|
||||||
final id = 'tag_${DateTime.now().millisecondsSinceEpoch}';
|
final id = 'tag_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
await db.insert('tags', {
|
await db.insert('tags', {
|
||||||
@@ -31,10 +42,10 @@ class TagDao {
|
|||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
});
|
});
|
||||||
return id;
|
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 db = await _dbHelper.database;
|
||||||
|
|
||||||
final tag = await getTagById(tagId);
|
final tag = await getTagById(tagId);
|
||||||
@@ -50,26 +61,27 @@ class TagDao {
|
|||||||
whereArgs: [newName, type, tagId]);
|
whereArgs: [newName, type, tagId]);
|
||||||
if (existing.isNotEmpty) return false;
|
if (existing.isNotEmpty) return false;
|
||||||
|
|
||||||
await db.update('tags', {'name': newName},
|
await db.transaction((txn) async {
|
||||||
where: 'id = ?', whereArgs: [tagId]);
|
await txn.update('tags', {'name': newName},
|
||||||
|
where: 'id = ?', whereArgs: [tagId]);
|
||||||
|
|
||||||
// 级联更新
|
switch (type) {
|
||||||
switch (type) {
|
case 'movie_genre':
|
||||||
case 'movie_genre':
|
await _cascadeRenameInMovies(txn, oldName, newName);
|
||||||
await _cascadeRenameInMovies(oldName, newName);
|
case 'book_genre':
|
||||||
case 'book_genre':
|
await _cascadeRenameInBooks(txn, oldName, newName);
|
||||||
await _cascadeRenameInBooks(oldName, newName);
|
case 'note_tag':
|
||||||
case 'note_tag':
|
await _cascadeRenameInNotes(txn, oldName, newName);
|
||||||
await _cascadeRenameInNotes(oldName, newName);
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 删除标签
|
/// 删除标签(事务保护)
|
||||||
/// [replacementName] 不为 null 时,先将所有条目中的旧标签替换为新标签,再删除
|
/// [replacementName] 不为 null 时,先将所有条目中的旧标签替换为新标签,再删除
|
||||||
/// [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 db = await _dbHelper.database;
|
||||||
|
|
||||||
final tag = await getTagById(tagId);
|
final tag = await getTagById(tagId);
|
||||||
@@ -78,137 +90,144 @@ class TagDao {
|
|||||||
final name = tag['name'] as String;
|
final name = tag['name'] as String;
|
||||||
final type = tag['type'] as String;
|
final type = tag['type'] as String;
|
||||||
|
|
||||||
if (replacementName != null && replacementName != name) {
|
await db.transaction((txn) async {
|
||||||
await _ensureTagExists(replacementName, type);
|
if (replacementName != null && replacementName != name) {
|
||||||
switch (type) {
|
await _ensureTagExists(txn, replacementName, type);
|
||||||
case 'movie_genre':
|
switch (type) {
|
||||||
await _cascadeRenameInMovies(name, replacementName);
|
case 'movie_genre':
|
||||||
case 'book_genre':
|
await _cascadeRenameInMovies(txn, name, replacementName);
|
||||||
await _cascadeRenameInBooks(name, replacementName);
|
case 'book_genre':
|
||||||
case 'note_tag':
|
await _cascadeRenameInBooks(txn, name, replacementName);
|
||||||
await _cascadeRenameInNotes(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;
|
final db = await _dbHelper.database;
|
||||||
await db.delete('tags', where: 'id = ?', whereArgs: [tagId]);
|
await db.delete('tags', where: 'id = ?', whereArgs: [tagId]);
|
||||||
}
|
});
|
||||||
|
|
||||||
/// 确保标签存在(用于替换操作)
|
/// 确保标签存在(用于替换操作)
|
||||||
Future<void> _ensureTagExists(String name, String type) async {
|
Future<void> _ensureTagExists(Transaction txn, String name, String type) async {
|
||||||
final db = await _dbHelper.database;
|
final existing = await txn.query('tags',
|
||||||
final existing = await db.query('tags',
|
|
||||||
where: 'name = ? AND type = ?', whereArgs: [name, type]);
|
where: 'name = ? AND type = ?', whereArgs: [name, type]);
|
||||||
if (existing.isEmpty) {
|
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 {
|
Future<void> _cascadeRenameInMovies(Transaction txn, String oldName, String newName) async {
|
||||||
final db = await _dbHelper.database;
|
final movies = await txn.query('movies',
|
||||||
final movies = await db.query('movies');
|
where: "genres LIKE ?", whereArgs: ['%$oldName%']);
|
||||||
|
final now = DateTime.now().toIso8601String();
|
||||||
for (final row in movies) {
|
for (final row in movies) {
|
||||||
final genres = _parseList(row['genres']);
|
final genres = _parseList(row['genres']);
|
||||||
if (genres.contains(oldName) && !genres.contains(newName)) {
|
if (!genres.contains(oldName)) continue;
|
||||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||||
await db.update('movies', {
|
await txn.update('movies', {
|
||||||
'genres': jsonEncode(updated),
|
'genres': jsonEncode(updated),
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
'updated_at': now,
|
||||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cascadeRenameInBooks(String oldName, String newName) async {
|
Future<void> _cascadeRenameInBooks(Transaction txn, String oldName, String newName) async {
|
||||||
final db = await _dbHelper.database;
|
final books = await txn.query('books',
|
||||||
final books = await db.query('books');
|
where: "genres LIKE ?", whereArgs: ['%$oldName%']);
|
||||||
|
final now = DateTime.now().toIso8601String();
|
||||||
for (final row in books) {
|
for (final row in books) {
|
||||||
final genres = _parseList(row['genres']);
|
final genres = _parseList(row['genres']);
|
||||||
if (genres.contains(oldName) && !genres.contains(newName)) {
|
if (!genres.contains(oldName)) continue;
|
||||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||||
await db.update('books', {
|
await txn.update('books', {
|
||||||
'genres': jsonEncode(updated),
|
'genres': jsonEncode(updated),
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
'updated_at': now,
|
||||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cascadeRenameInNotes(String oldName, String newName) async {
|
Future<void> _cascadeRenameInNotes(Transaction txn, String oldName, String newName) async {
|
||||||
final db = await _dbHelper.database;
|
final notes = await txn.query('notes',
|
||||||
final notes = await db.query('notes');
|
where: "tags LIKE ?", whereArgs: ['%$oldName%']);
|
||||||
|
final now = DateTime.now().toIso8601String();
|
||||||
for (final row in notes) {
|
for (final row in notes) {
|
||||||
final tags = _parseList(row['tags']);
|
final tags = _parseList(row['tags']);
|
||||||
if (tags.contains(oldName) && !tags.contains(newName)) {
|
if (!tags.contains(oldName)) continue;
|
||||||
final updated = tags.map((t) => t == oldName ? newName : t).toList();
|
final updated = tags.map((t) => t == oldName ? newName : t).toList();
|
||||||
await db.update('notes', {
|
await txn.update('notes', {
|
||||||
'tags': jsonEncode(updated),
|
'tags': jsonEncode(updated),
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
'updated_at': now,
|
||||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== 级联删除 ======
|
// ====== 级联删除 ======
|
||||||
|
|
||||||
Future<void> _cascadeDeleteFromMovies(String tagName) async {
|
Future<void> _cascadeDeleteFromMovies(Transaction txn, String tagName) async {
|
||||||
final db = await _dbHelper.database;
|
final movies = await txn.query('movies',
|
||||||
final movies = await db.query('movies');
|
where: "genres LIKE ?", whereArgs: ['%$tagName%']);
|
||||||
|
final now = DateTime.now().toIso8601String();
|
||||||
for (final row in movies) {
|
for (final row in movies) {
|
||||||
final genres = _parseList(row['genres']);
|
final genres = _parseList(row['genres']);
|
||||||
if (genres.contains(tagName)) {
|
if (!genres.contains(tagName)) continue;
|
||||||
genres.removeWhere((g) => g == tagName);
|
genres.removeWhere((g) => g == tagName);
|
||||||
await db.update('movies', {
|
await txn.update('movies', {
|
||||||
'genres': jsonEncode(genres),
|
'genres': jsonEncode(genres),
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
'updated_at': now,
|
||||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cascadeDeleteFromBooks(String tagName) async {
|
Future<void> _cascadeDeleteFromBooks(Transaction txn, String tagName) async {
|
||||||
final db = await _dbHelper.database;
|
final books = await txn.query('books',
|
||||||
final books = await db.query('books');
|
where: "genres LIKE ?", whereArgs: ['%$tagName%']);
|
||||||
|
final now = DateTime.now().toIso8601String();
|
||||||
for (final row in books) {
|
for (final row in books) {
|
||||||
final genres = _parseList(row['genres']);
|
final genres = _parseList(row['genres']);
|
||||||
if (genres.contains(tagName)) {
|
if (!genres.contains(tagName)) continue;
|
||||||
genres.removeWhere((g) => g == tagName);
|
genres.removeWhere((g) => g == tagName);
|
||||||
await db.update('books', {
|
await txn.update('books', {
|
||||||
'genres': jsonEncode(genres),
|
'genres': jsonEncode(genres),
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
'updated_at': now,
|
||||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cascadeDeleteFromNotes(String tagName) async {
|
Future<void> _cascadeDeleteFromNotes(Transaction txn, String tagName) async {
|
||||||
final db = await _dbHelper.database;
|
final notes = await txn.query('notes',
|
||||||
final notes = await db.query('notes');
|
where: "tags LIKE ?", whereArgs: ['%$tagName%']);
|
||||||
|
final now = DateTime.now().toIso8601String();
|
||||||
for (final row in notes) {
|
for (final row in notes) {
|
||||||
final tags = _parseList(row['tags']);
|
final tags = _parseList(row['tags']);
|
||||||
if (tags.contains(tagName)) {
|
if (!tags.contains(tagName)) continue;
|
||||||
tags.removeWhere((t) => t == tagName);
|
tags.removeWhere((t) => t == tagName);
|
||||||
await db.update('notes', {
|
await txn.update('notes', {
|
||||||
'tags': jsonEncode(tags),
|
'tags': jsonEncode(tags),
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
'updated_at': now,
|
||||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class AppTheme {
|
|||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _black,
|
color: _black,
|
||||||
letterSpacing: -0.3,
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ class AppTheme {
|
|||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _black,
|
color: _black,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: 0,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
headlineMedium: TextStyle(
|
headlineMedium: TextStyle(
|
||||||
@@ -192,7 +192,7 @@ class AppTheme {
|
|||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _black,
|
color: _black,
|
||||||
letterSpacing: -0.3,
|
letterSpacing: 0,
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
headlineSmall: TextStyle(
|
headlineSmall: TextStyle(
|
||||||
@@ -200,7 +200,7 @@ class AppTheme {
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _black,
|
color: _black,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: 0,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
// 正文
|
// 正文
|
||||||
@@ -282,7 +282,7 @@ class AppTheme {
|
|||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _white,
|
color: _white,
|
||||||
letterSpacing: -0.3,
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ class AppTheme {
|
|||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _white,
|
color: _white,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: 0,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
headlineMedium: TextStyle(
|
headlineMedium: TextStyle(
|
||||||
@@ -388,7 +388,7 @@ class AppTheme {
|
|||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _white,
|
color: _white,
|
||||||
letterSpacing: -0.3,
|
letterSpacing: 0,
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
headlineSmall: TextStyle(
|
headlineSmall: TextStyle(
|
||||||
@@ -396,7 +396,7 @@ class AppTheme {
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: _semibold,
|
fontWeight: _semibold,
|
||||||
color: _white,
|
color: _white,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: 0,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
bodyLarge: TextStyle(
|
bodyLarge: TextStyle(
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ class UserPrefs {
|
|||||||
|
|
||||||
// ========== 应用图标设置 ==========
|
// ========== 应用图标设置 ==========
|
||||||
|
|
||||||
|
// ========== Markdown 阅读器 ==========
|
||||||
|
|
||||||
/// Markdown 阅读器最近选择的目录
|
/// Markdown 阅读器最近选择的目录
|
||||||
String? get lastMdFolder => prefs.getString('lastMdFolder');
|
String? get lastMdFolder => prefs.getString('lastMdFolder');
|
||||||
Future<bool> setLastMdFolder(String value) => prefs.setString('lastMdFolder', value);
|
Future<bool> setLastMdFolder(String value) => prefs.setString('lastMdFolder', value);
|
||||||
|
|||||||
400
pubspec.lock
400
pubspec.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user