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;
|
||||
}
|
||||
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (widget.review == null) {
|
||||
@@ -264,5 +265,9 @@ class _BookReviewFormPageState extends State<BookReviewFormPage> {
|
||||
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ToastUtil.show(context, '保存失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
int _lastDataCount = -1;
|
||||
DateTime? _lastUpdatedAt;
|
||||
late ScrollController _scrollController;
|
||||
AppProvider? _provider;
|
||||
|
||||
static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
||||
|
||||
@@ -38,6 +39,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
_scrollController = ScrollController()..addListener(_onScroll);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = context.read<AppProvider>();
|
||||
_provider = provider;
|
||||
provider.addListener(_onDataChanged);
|
||||
_lastDataCount = provider.books.length;
|
||||
if (provider.books.isNotEmpty) _lastUpdatedAt = provider.books.first.updatedAt;
|
||||
@@ -47,6 +49,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_provider?.removeListener(_onDataChanged);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -1806,6 +1806,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 收集所有多值字段输入框中的未提交内容
|
||||
_collectUnsubmittedValues();
|
||||
|
||||
@@ -1869,6 +1870,10 @@ class _MovieFormPageState extends State<MovieFormPage> {
|
||||
ToastUtil.show(context, widget.movie == null ? '添加成功' : '更新成功');
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ToastUtil.show(context, '保存失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 将海报从临时ID目录移动到新的影视ID目录
|
||||
|
||||
@@ -303,6 +303,7 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
Future<void> _saveReview() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (widget.review == null) {
|
||||
@@ -331,5 +332,9 @@ class _MovieReviewFormPageState extends State<MovieReviewFormPage> {
|
||||
if (!mounted) return;
|
||||
ToastUtil.show(context, widget.review == null ? '添加成功' : '更新成功');
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ToastUtil.show(context, '保存失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
int _lastDataCount = -1;
|
||||
DateTime? _lastUpdatedAt;
|
||||
late ScrollController _scrollController;
|
||||
AppProvider? _provider;
|
||||
|
||||
static const _statusMap = {0: 'watched', 1: 'watching', 2: 'want_to_watch'};
|
||||
|
||||
@@ -38,6 +39,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
_scrollController = ScrollController()..addListener(_onScroll);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = context.read<AppProvider>();
|
||||
_provider = provider;
|
||||
provider.addListener(_onDataChanged);
|
||||
_lastDataCount = provider.movies.length;
|
||||
if (provider.movies.isNotEmpty) _lastUpdatedAt = provider.movies.first.updatedAt;
|
||||
@@ -47,6 +49,7 @@ class _MovieTabPageState extends State<MovieTabPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_provider?.removeListener(_onDataChanged);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -825,6 +825,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (_isEditing) {
|
||||
@@ -873,6 +874,10 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ToastUtil.show(context, '保存失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 将图片从临时ID目录移动到新ID目录
|
||||
|
||||
@@ -21,6 +21,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
bool _isLoading = false;
|
||||
int _offset = 0;
|
||||
late ScrollController _scrollController;
|
||||
AppProvider? _provider;
|
||||
int _layoutStyle = 0;
|
||||
bool _initialized = false;
|
||||
int _lastDataCount = -1;
|
||||
@@ -33,6 +34,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
_scrollController = ScrollController()..addListener(_onScroll);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = context.read<AppProvider>();
|
||||
_provider = provider;
|
||||
provider.addListener(_onDataChanged);
|
||||
_lastDataCount = provider.notes.length;
|
||||
if (provider.notes.isNotEmpty) _lastUpdatedAt = provider.notes.first.updatedAt;
|
||||
@@ -42,6 +44,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_provider?.removeListener(_onDataChanged);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'slide_up_page_route.dart';
|
||||
import '../pages/movies/movie_form_page.dart';
|
||||
import '../pages/book/book_form_page.dart';
|
||||
import '../pages/note/note_form_page.dart';
|
||||
@@ -7,7 +8,6 @@ import '../pages/movies/movie_detail_page.dart';
|
||||
import '../pages/book/book_detail_page.dart';
|
||||
import '../pages/note/note_detail_page.dart';
|
||||
import '../pages/movies/douban_webview_page.dart';
|
||||
import 'slide_up_page_route.dart';
|
||||
|
||||
/// 路由生成器
|
||||
class AppRouter {
|
||||
@@ -15,56 +15,65 @@ class AppRouter {
|
||||
switch (settings.name) {
|
||||
case '/movie-form':
|
||||
final args = settings.arguments;
|
||||
Movie? movie;
|
||||
String? initialStatus;
|
||||
if (args is Movie) {
|
||||
movie = args;
|
||||
} else if (args is Map<String, dynamic>) {
|
||||
initialStatus = args['initialStatus'] as String?;
|
||||
}
|
||||
final Movie? movie = args is Movie ? args : null;
|
||||
final String? initialStatus =
|
||||
args is Map<String, dynamic> ? (args['initialStatus'] as String?) : null;
|
||||
return SlideUpPageRoute(
|
||||
page: MovieFormPage(movie: movie, initialStatus: initialStatus),
|
||||
);
|
||||
|
||||
case '/book-form':
|
||||
final args = settings.arguments;
|
||||
Book? book;
|
||||
String? initialStatus;
|
||||
if (args is Book) {
|
||||
book = args;
|
||||
} else if (args is Map<String, dynamic>) {
|
||||
initialStatus = args['initialStatus'] as String?;
|
||||
}
|
||||
final Book? book = args is Book ? args : null;
|
||||
final String? initialStatus =
|
||||
args is Map<String, dynamic> ? (args['initialStatus'] as String?) : null;
|
||||
return SlideUpPageRoute(
|
||||
page: BookFormPage(book: book, initialStatus: initialStatus),
|
||||
);
|
||||
|
||||
case '/note-form':
|
||||
final note = settings.arguments as Note?;
|
||||
final args = settings.arguments;
|
||||
final Note? note = args is Note ? args : null;
|
||||
return SlideUpPageRoute(page: NoteFormPage(note: note));
|
||||
|
||||
case '/movie-detail':
|
||||
final movie = settings.arguments as Movie;
|
||||
final movie = settings.arguments is Movie ? settings.arguments as Movie : null;
|
||||
if (movie == null) {
|
||||
return _buildUnknownRoute(settings.name);
|
||||
}
|
||||
return SlideUpPageRoute(page: MovieDetailPage(movie: movie));
|
||||
|
||||
case '/book-detail':
|
||||
final book = settings.arguments as Book;
|
||||
final book = settings.arguments is Book ? settings.arguments as Book : null;
|
||||
if (book == null) {
|
||||
return _buildUnknownRoute(settings.name);
|
||||
}
|
||||
return SlideUpPageRoute(page: BookDetailPage(book: book));
|
||||
|
||||
case '/note-detail':
|
||||
final note = settings.arguments as Note;
|
||||
final note = settings.arguments is Note ? settings.arguments as Note : null;
|
||||
if (note == null) {
|
||||
return _buildUnknownRoute(settings.name);
|
||||
}
|
||||
return SlideUpPageRoute(page: NoteDetailPage(note: note));
|
||||
|
||||
case '/douban-webview':
|
||||
final url = settings.arguments as String;
|
||||
final url = settings.arguments is String ? settings.arguments as String : null;
|
||||
if (url == null) {
|
||||
return _buildUnknownRoute(settings.name);
|
||||
}
|
||||
return SlideUpPageRoute(page: DoubanWebViewPage(url: url));
|
||||
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: Center(child: Text('未找到页面:${settings.name}')),
|
||||
),
|
||||
);
|
||||
return _buildUnknownRoute(settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
static Route<dynamic> _buildUnknownRoute(String? name) {
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: Center(child: Text('未找到页面:${name ?? ''}')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
||||
class BookDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[BookDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有未删除的书籍记录
|
||||
Future<List<Book>> getAllBooks() async {
|
||||
Future<List<Book>> getAllBooks() => _wrap('getAllBooks', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
@@ -15,12 +24,11 @@ class BookDao {
|
||||
whereArgs: [0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 分页查询书籍记录
|
||||
Future<List<Book>> getBooksPaged({String? status, int limit = 20, int offset = 0}) async {
|
||||
Future<List<Book>> getBooksPaged({String? status, int limit = 20, int offset = 0}) => _wrap('getBooksPaged', () async {
|
||||
final db = await _dbHelper.database;
|
||||
String where = 'is_deleted = 0';
|
||||
List<dynamic> whereArgs = [];
|
||||
@@ -31,10 +39,10 @@ class BookDao {
|
||||
final maps = await db.query('books', where: where, whereArgs: whereArgs,
|
||||
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据状态筛选书籍记录
|
||||
Future<List<Book>> getBooksByStatus(String status) async {
|
||||
Future<List<Book>> getBooksByStatus(String status) => _wrap('getBooksByStatus', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
@@ -42,31 +50,29 @@ class BookDao {
|
||||
whereArgs: [status, 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据ID获取书籍
|
||||
Future<Book?> getBookById(String id) async {
|
||||
Future<Book?> getBookById(String id) => _wrap('getBookById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'id = ? AND is_deleted = ?',
|
||||
whereArgs: [id, 0],
|
||||
);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return Book.fromJson(maps.first);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加书籍记录
|
||||
Future<int> insertBook(Book book) async {
|
||||
Future<int> insertBook(Book book) => _wrap('insertBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('books', book.toJson());
|
||||
}
|
||||
});
|
||||
|
||||
// 更新书籍记录
|
||||
Future<int> updateBook(Book book) async {
|
||||
Future<int> updateBook(Book book) => _wrap('updateBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'books',
|
||||
@@ -74,10 +80,10 @@ class BookDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [book.id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 软删除书籍记录(移入回收站)
|
||||
Future<int> deleteBook(String id) async {
|
||||
Future<int> deleteBook(String id) => _wrap('deleteBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'books',
|
||||
@@ -88,10 +94,10 @@ class BookDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索书籍(标题、别名)
|
||||
Future<List<Book>> searchBooks(String query) async {
|
||||
Future<List<Book>> searchBooks(String query) => _wrap('searchBooks', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
@@ -99,12 +105,11 @@ class BookDao {
|
||||
whereArgs: ['%$query%', '%$query%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据作者筛选
|
||||
Future<List<Book>> getBooksByAuthor(String author) async {
|
||||
Future<List<Book>> getBooksByAuthor(String author) => _wrap('getBooksByAuthor', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
@@ -112,12 +117,11 @@ class BookDao {
|
||||
whereArgs: ['%$author%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据类型筛选
|
||||
Future<List<Book>> getBooksByGenre(String genre) async {
|
||||
Future<List<Book>> getBooksByGenre(String genre) => _wrap('getBooksByGenre', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
@@ -125,14 +129,13 @@ class BookDao {
|
||||
whereArgs: ['%$genre%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的书籍
|
||||
Future<List<Book>> getDeletedBooks() async {
|
||||
Future<List<Book>> getDeletedBooks() => _wrap('getDeletedBooks', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
@@ -140,12 +143,11 @@ class BookDao {
|
||||
whereArgs: [1],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复已删除的书籍
|
||||
Future<int> restoreBook(String id) async {
|
||||
Future<int> restoreBook(String id) => _wrap('restoreBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'books',
|
||||
@@ -153,15 +155,15 @@ class BookDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 彻底删除书籍
|
||||
Future<int> permanentDeleteBook(String id) async {
|
||||
Future<int> permanentDeleteBook(String id) => _wrap('permanentDeleteBook', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'books',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
@@ -6,8 +7,17 @@ import '../database_helper.dart';
|
||||
class BookExcerptDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[BookExcerptDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取书籍的所有摘抄
|
||||
Future<List<BookExcerpt>> getExcerptsByBookId(String bookId) async {
|
||||
Future<List<BookExcerpt>> getExcerptsByBookId(String bookId) => _wrap('getExcerptsByBookId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_excerpts',
|
||||
@@ -16,24 +26,22 @@ class BookExcerptDao {
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
|
||||
}
|
||||
});
|
||||
|
||||
/// 根据ID获取摘抄
|
||||
Future<BookExcerpt?> getExcerptById(String id) async {
|
||||
Future<BookExcerpt?> getExcerptById(String id) => _wrap('getExcerptById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_excerpts',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isNotEmpty) {
|
||||
return BookExcerpt.fromJson(maps.first);
|
||||
}
|
||||
if (maps.isNotEmpty) return BookExcerpt.fromJson(maps.first);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/// 插入摘抄
|
||||
Future<String> insertExcerpt(BookExcerpt excerpt) async {
|
||||
Future<String> insertExcerpt(BookExcerpt excerpt) => _wrap('insertExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
'book_excerpts',
|
||||
@@ -41,10 +49,10 @@ class BookExcerptDao {
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return excerpt.id;
|
||||
}
|
||||
});
|
||||
|
||||
/// 更新摘抄
|
||||
Future<void> updateExcerpt(BookExcerpt excerpt) async {
|
||||
Future<void> updateExcerpt(BookExcerpt excerpt) => _wrap('updateExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
@@ -52,10 +60,10 @@ class BookExcerptDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [excerpt.id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 删除摘抄(软删除)
|
||||
Future<void> deleteExcerpt(String id) async {
|
||||
Future<void> deleteExcerpt(String id) => _wrap('deleteExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
@@ -63,30 +71,30 @@ class BookExcerptDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 彻底删除摘抄
|
||||
Future<void> permanentDeleteExcerpt(String id) async {
|
||||
Future<void> permanentDeleteExcerpt(String id) => _wrap('permanentDeleteExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'book_excerpts',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 获取摘抄数量
|
||||
Future<int> getExcerptCount(String bookId) async {
|
||||
Future<int> getExcerptCount(String bookId) => _wrap('getExcerptCount', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM book_excerpts WHERE book_id = ? AND is_deleted = 0',
|
||||
[bookId],
|
||||
);
|
||||
return result.first['count'] as int? ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
/// 获取所有已删除的摘抄
|
||||
Future<List<BookExcerpt>> getDeletedExcerpts() async {
|
||||
Future<List<BookExcerpt>> getDeletedExcerpts() => _wrap('getDeletedExcerpts', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_excerpts',
|
||||
@@ -94,10 +102,10 @@ class BookExcerptDao {
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookExcerpt.fromJson(map)).toList();
|
||||
}
|
||||
});
|
||||
|
||||
/// 恢复已删除的摘抄
|
||||
Future<void> restoreExcerpt(String id) async {
|
||||
Future<void> restoreExcerpt(String id) => _wrap('restoreExcerpt', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_excerpts',
|
||||
@@ -105,5 +113,5 @@ class BookExcerptDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
@@ -6,8 +7,17 @@ import '../database_helper.dart';
|
||||
class BookReviewDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[BookReviewDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取书籍的所有书评
|
||||
Future<List<BookReview>> getReviewsByBookId(String bookId) async {
|
||||
Future<List<BookReview>> getReviewsByBookId(String bookId) => _wrap('getReviewsByBookId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_reviews',
|
||||
@@ -16,24 +26,22 @@ class BookReviewDao {
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookReview.fromJson(map)).toList();
|
||||
}
|
||||
});
|
||||
|
||||
/// 根据ID获取书评
|
||||
Future<BookReview?> getReviewById(String id) async {
|
||||
Future<BookReview?> getReviewById(String id) => _wrap('getReviewById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_reviews',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isNotEmpty) {
|
||||
return BookReview.fromJson(maps.first);
|
||||
}
|
||||
if (maps.isNotEmpty) return BookReview.fromJson(maps.first);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/// 插入书评
|
||||
Future<String> insertReview(BookReview review) async {
|
||||
Future<String> insertReview(BookReview review) => _wrap('insertReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
'book_reviews',
|
||||
@@ -41,10 +49,10 @@ class BookReviewDao {
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return review.id;
|
||||
}
|
||||
});
|
||||
|
||||
/// 更新书评
|
||||
Future<void> updateReview(BookReview review) async {
|
||||
Future<void> updateReview(BookReview review) => _wrap('updateReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
@@ -52,10 +60,10 @@ class BookReviewDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [review.id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 删除书评(软删除)
|
||||
Future<void> deleteReview(String id) async {
|
||||
Future<void> deleteReview(String id) => _wrap('deleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
@@ -63,30 +71,30 @@ class BookReviewDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 彻底删除书评
|
||||
Future<void> permanentDeleteReview(String id) async {
|
||||
Future<void> permanentDeleteReview(String id) => _wrap('permanentDeleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'book_reviews',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 获取书评数量
|
||||
Future<int> getReviewCount(String bookId) async {
|
||||
Future<int> getReviewCount(String bookId) => _wrap('getReviewCount', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM book_reviews WHERE book_id = ? AND is_deleted = 0',
|
||||
[bookId],
|
||||
);
|
||||
return result.first['count'] as int? ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
/// 获取所有已删除的书评
|
||||
Future<List<BookReview>> getDeletedReviews() async {
|
||||
Future<List<BookReview>> getDeletedReviews() => _wrap('getDeletedReviews', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'book_reviews',
|
||||
@@ -94,10 +102,10 @@ class BookReviewDao {
|
||||
orderBy: 'updated_at DESC',
|
||||
);
|
||||
return maps.map((map) => BookReview.fromJson(map)).toList();
|
||||
}
|
||||
});
|
||||
|
||||
/// 恢复已删除的书评
|
||||
Future<void> restoreReview(String id) async {
|
||||
Future<void> restoreReview(String id) => _wrap('restoreReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.update(
|
||||
'book_reviews',
|
||||
@@ -105,5 +113,5 @@ class BookReviewDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 数据库帮助类 - 管理数据库的创建和版本控制
|
||||
@@ -26,6 +28,23 @@ class DatabaseHelper {
|
||||
_database = await _initDB('mooknote.db');
|
||||
}
|
||||
|
||||
/// 从备份字节重写数据库文件并安全重连,
|
||||
/// 始终按当前代码的 targetVersion 重新初始化,避免版本不一致。
|
||||
Future<void> reopenDatabaseFromBytes(Uint8List bytes) async {
|
||||
await close();
|
||||
|
||||
final dbPath = await getDatabasesPath();
|
||||
final path = join(dbPath, 'mooknote.db');
|
||||
|
||||
final dbFile = File(path);
|
||||
if (await dbFile.exists()) {
|
||||
await dbFile.delete();
|
||||
}
|
||||
|
||||
await dbFile.writeAsBytes(bytes, flush: true);
|
||||
_database = await _initDB('mooknote.db');
|
||||
}
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await _initDB('mooknote.db');
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 图片路径管理助手 - 按分类和ID组织图片存储
|
||||
///
|
||||
///
|
||||
/// 存储结构:
|
||||
/// images/movies/{movieId}/xxxx.jpg - 影视海报
|
||||
/// images/movies/{movieId}/posterimgs/xxxx.jpg - 影视海报墙图片
|
||||
@@ -11,11 +12,11 @@ import 'package:path/path.dart' as p;
|
||||
/// images/notes/{noteId}/xxxx.jpg - 笔记图片
|
||||
class ImagePathHelper {
|
||||
static final ImagePathHelper instance = ImagePathHelper._init();
|
||||
|
||||
|
||||
ImagePathHelper._init();
|
||||
|
||||
|
||||
String? _appDirPath;
|
||||
|
||||
|
||||
/// 获取应用文档目录
|
||||
Future<String> get _appDir async {
|
||||
if (_appDirPath != null) return _appDirPath!;
|
||||
@@ -23,141 +24,166 @@ class ImagePathHelper {
|
||||
_appDirPath = appDir.path;
|
||||
return _appDirPath!;
|
||||
}
|
||||
|
||||
|
||||
/// 获取图片根目录
|
||||
Future<String> get imagesRoot async {
|
||||
final appDir = await _appDir;
|
||||
return p.join(appDir, 'images');
|
||||
}
|
||||
|
||||
|
||||
// ==================== 影视相关路径 ====================
|
||||
|
||||
|
||||
/// 获取影视图片目录
|
||||
/// 路径: images/movies/{movieId}/
|
||||
Future<String> getMovieImagesDir(String movieId) async {
|
||||
final root = await imagesRoot;
|
||||
return p.join(root, 'movies', movieId);
|
||||
}
|
||||
|
||||
|
||||
/// 获取影视海报路径
|
||||
/// 路径: images/movies/{movieId}/{fileName}
|
||||
Future<String> getMoviePosterPath(String movieId, String fileName) async {
|
||||
final dir = await getMovieImagesDir(movieId);
|
||||
return p.join(dir, fileName);
|
||||
}
|
||||
|
||||
|
||||
/// 获取影视海报墙目录
|
||||
/// 路径: images/movies/{movieId}/posterimgs/
|
||||
Future<String> getMoviePosterImgsDir(String movieId) async {
|
||||
final dir = await getMovieImagesDir(movieId);
|
||||
return p.join(dir, 'posterimgs');
|
||||
}
|
||||
|
||||
|
||||
/// 获取影视海报墙图片路径
|
||||
/// 路径: images/movies/{movieId}/posterimgs/{fileName}
|
||||
Future<String> getMoviePosterImgPath(String movieId, String fileName) async {
|
||||
final dir = await getMoviePosterImgsDir(movieId);
|
||||
return p.join(dir, fileName);
|
||||
}
|
||||
|
||||
|
||||
// ==================== 书籍相关路径 ====================
|
||||
|
||||
|
||||
/// 获取书籍图片目录
|
||||
/// 路径: images/books/{bookId}/
|
||||
Future<String> getBookImagesDir(String bookId) async {
|
||||
final root = await imagesRoot;
|
||||
return p.join(root, 'books', bookId);
|
||||
}
|
||||
|
||||
|
||||
/// 获取书籍封面路径
|
||||
/// 路径: images/books/{bookId}/{fileName}
|
||||
Future<String> getBookCoverPath(String bookId, String fileName) async {
|
||||
final dir = await getBookImagesDir(bookId);
|
||||
return p.join(dir, fileName);
|
||||
}
|
||||
|
||||
|
||||
// ==================== 笔记相关路径 ====================
|
||||
|
||||
|
||||
/// 获取笔记图片目录
|
||||
/// 路径: images/notes/{noteId}/
|
||||
Future<String> getNoteImagesDir(String noteId) async {
|
||||
final root = await imagesRoot;
|
||||
return p.join(root, 'notes', noteId);
|
||||
}
|
||||
|
||||
|
||||
/// 获取笔记图片路径
|
||||
/// 路径: images/notes/{noteId}/{fileName}
|
||||
Future<String> getNoteImagePath(String noteId, String fileName) async {
|
||||
final dir = await getNoteImagesDir(noteId);
|
||||
return p.join(dir, fileName);
|
||||
}
|
||||
|
||||
|
||||
// ==================== 目录操作 ====================
|
||||
|
||||
|
||||
/// 确保目录存在
|
||||
Future<void> ensureDirExists(String dirPath) async {
|
||||
final dir = Directory(dirPath);
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
try {
|
||||
final dir = Directory(dirPath);
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ImagePathHelper] ensureDirExists($dirPath) error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 删除影视图片目录(包括海报和海报墙)
|
||||
/// 删除路径: images/movies/{movieId}/
|
||||
Future<void> deleteMovieImages(String movieId) async {
|
||||
final dirPath = await getMovieImagesDir(movieId);
|
||||
await _deleteDirectory(dirPath);
|
||||
}
|
||||
|
||||
|
||||
/// 删除书籍图片目录
|
||||
/// 删除路径: images/books/{bookId}/
|
||||
Future<void> deleteBookImages(String bookId) async {
|
||||
final dirPath = await getBookImagesDir(bookId);
|
||||
await _deleteDirectory(dirPath);
|
||||
}
|
||||
|
||||
|
||||
/// 删除笔记图片目录
|
||||
/// 删除路径: images/notes/{noteId}/
|
||||
Future<void> deleteNoteImages(String noteId) async {
|
||||
final dirPath = await getNoteImagesDir(noteId);
|
||||
await _deleteDirectory(dirPath);
|
||||
}
|
||||
|
||||
|
||||
/// 删除目录及其内容
|
||||
Future<void> _deleteDirectory(String dirPath) async {
|
||||
final dir = Directory(dirPath);
|
||||
if (await dir.exists()) {
|
||||
await dir.delete(recursive: true);
|
||||
try {
|
||||
final dir = Directory(dirPath);
|
||||
if (await dir.exists()) {
|
||||
await dir.delete(recursive: true);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ImagePathHelper] deleteDirectory($dirPath) error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 移动文件到新位置
|
||||
Future<String> moveFile(String sourcePath, String targetDir, String fileName) async {
|
||||
await ensureDirExists(targetDir);
|
||||
final targetPath = p.join(targetDir, fileName);
|
||||
final sourceFile = File(sourcePath);
|
||||
if (await sourceFile.exists()) {
|
||||
await sourceFile.rename(targetPath);
|
||||
try {
|
||||
await ensureDirExists(targetDir);
|
||||
final targetPath = p.join(targetDir, fileName);
|
||||
final sourceFile = File(sourcePath);
|
||||
if (await sourceFile.exists()) {
|
||||
await sourceFile.rename(targetPath);
|
||||
}
|
||||
return targetPath;
|
||||
} catch (e) {
|
||||
debugPrint('[ImagePathHelper] moveFile($sourcePath) error: $e');
|
||||
rethrow;
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
|
||||
/// 复制文件到新位置
|
||||
Future<String> copyFile(String sourcePath, String targetDir, String fileName) async {
|
||||
await ensureDirExists(targetDir);
|
||||
final targetPath = p.join(targetDir, fileName);
|
||||
final sourceFile = File(sourcePath);
|
||||
if (await sourceFile.exists()) {
|
||||
await sourceFile.copy(targetPath);
|
||||
try {
|
||||
await ensureDirExists(targetDir);
|
||||
final targetPath = p.join(targetDir, fileName);
|
||||
final sourceFile = File(sourcePath);
|
||||
if (await sourceFile.exists()) {
|
||||
await sourceFile.copy(targetPath);
|
||||
}
|
||||
return targetPath;
|
||||
} catch (e) {
|
||||
debugPrint('[ImagePathHelper] copyFile($sourcePath) error: $e');
|
||||
rethrow;
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
|
||||
/// 删除单个文件
|
||||
Future<void> deleteFile(String filePath) async {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ImagePathHelper] deleteFile($filePath) error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
||||
class MovieDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[MovieDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有影视记录(未删除的)
|
||||
Future<List<Movie>> getAllMovies() async {
|
||||
Future<List<Movie>> getAllMovies() => _wrap('getAllMovies', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
@@ -15,12 +24,11 @@ class MovieDao {
|
||||
whereArgs: [0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 分页查询影视记录
|
||||
Future<List<Movie>> getMoviesPaged({String? status, int limit = 20, int offset = 0}) async {
|
||||
Future<List<Movie>> getMoviesPaged({String? status, int limit = 20, int offset = 0}) => _wrap('getMoviesPaged', () async {
|
||||
final db = await _dbHelper.database;
|
||||
String where = 'is_deleted = 0';
|
||||
List<dynamic> whereArgs = [];
|
||||
@@ -31,10 +39,10 @@ class MovieDao {
|
||||
final maps = await db.query('movies', where: where, whereArgs: whereArgs,
|
||||
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据状态筛选影视记录
|
||||
Future<List<Movie>> getMoviesByStatus(String status) async {
|
||||
Future<List<Movie>> getMoviesByStatus(String status) => _wrap('getMoviesByStatus', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
@@ -42,96 +50,86 @@ class MovieDao {
|
||||
whereArgs: [status, 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据导演筛选
|
||||
Future<List<Movie>> getMoviesByDirector(String director) async {
|
||||
Future<List<Movie>> getMoviesByDirector(String director) => _wrap('getMoviesByDirector', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
where: 'directors LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$director%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.directors.contains(director))
|
||||
.toList();
|
||||
}
|
||||
});
|
||||
|
||||
// 根据编剧筛选
|
||||
Future<List<Movie>> getMoviesByWriter(String writer) async {
|
||||
Future<List<Movie>> getMoviesByWriter(String writer) => _wrap('getMoviesByWriter', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
where: 'writers LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$writer%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.writers.contains(writer))
|
||||
.toList();
|
||||
}
|
||||
});
|
||||
|
||||
// 根据演员筛选
|
||||
Future<List<Movie>> getMoviesByActor(String actor) async {
|
||||
Future<List<Movie>> getMoviesByActor(String actor) => _wrap('getMoviesByActor', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
where: 'actors LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$actor%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.actors.contains(actor))
|
||||
.toList();
|
||||
}
|
||||
});
|
||||
|
||||
// 根据类型筛选
|
||||
Future<List<Movie>> getMoviesByGenre(String genre) async {
|
||||
Future<List<Movie>> getMoviesByGenre(String genre) => _wrap('getMoviesByGenre', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
where: 'genres LIKE ? AND is_deleted = ?',
|
||||
whereArgs: ['%$genre%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
||||
return maps.map((m) => Movie.fromJson(m))
|
||||
.where((movie) => movie.genres.contains(genre))
|
||||
.toList();
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索影视(标题或别名)
|
||||
Future<List<Movie>> searchMovies(String keyword) async {
|
||||
Future<List<Movie>> searchMovies(String keyword) => _wrap('searchMovies', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final likeKeyword = '%$keyword%';
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'is_deleted = ?',
|
||||
whereArgs: [0],
|
||||
where: '(title LIKE ? OR alternate_titles LIKE ?) AND is_deleted = ?',
|
||||
whereArgs: [likeKeyword, likeKeyword, 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
final lowerKeyword = keyword.toLowerCase();
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]))
|
||||
.where((movie) =>
|
||||
movie.title.toLowerCase().contains(lowerKeyword) ||
|
||||
movie.alternateTitles.any((t) => t.toLowerCase().contains(lowerKeyword)))
|
||||
.toList();
|
||||
}
|
||||
return maps.map((m) => Movie.fromJson(m)).toList();
|
||||
});
|
||||
|
||||
// 添加影视记录
|
||||
Future<int> insertMovie(Movie movie) async {
|
||||
Future<int> insertMovie(Movie movie) => _wrap('insertMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movies', movie.toJson());
|
||||
}
|
||||
});
|
||||
|
||||
// 更新影视记录
|
||||
Future<int> updateMovie(Movie movie) async {
|
||||
Future<int> updateMovie(Movie movie) => _wrap('updateMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movies',
|
||||
@@ -139,10 +137,10 @@ class MovieDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [movie.id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除影视记录(软删除)
|
||||
Future<int> deleteMovie(String id) async {
|
||||
Future<int> deleteMovie(String id) => _wrap('deleteMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movies',
|
||||
@@ -150,52 +148,73 @@ class MovieDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有导演(去重)
|
||||
Future<List<String>> getAllDirectors() async {
|
||||
Future<List<String>> getAllDirectors() => _wrap('getAllDirectors', () async {
|
||||
final movies = await getAllMovies();
|
||||
final directors = <String>{};
|
||||
for (final movie in movies) {
|
||||
directors.addAll(movie.directors);
|
||||
}
|
||||
return directors.toList()..sort();
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有编剧(去重)
|
||||
Future<List<String>> getAllWriters() async {
|
||||
Future<List<String>> getAllWriters() => _wrap('getAllWriters', () async {
|
||||
final movies = await getAllMovies();
|
||||
final writers = <String>{};
|
||||
for (final movie in movies) {
|
||||
writers.addAll(movie.writers);
|
||||
}
|
||||
return writers.toList()..sort();
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有演员(去重)
|
||||
Future<List<String>> getAllActors() async {
|
||||
Future<List<String>> getAllActors() => _wrap('getAllActors', () async {
|
||||
final movies = await getAllMovies();
|
||||
final actors = <String>{};
|
||||
for (final movie in movies) {
|
||||
actors.addAll(movie.actors);
|
||||
}
|
||||
return actors.toList()..sort();
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有类型(去重)
|
||||
Future<List<String>> getAllGenres() async {
|
||||
Future<List<String>> getAllGenres() => _wrap('getAllGenres', () async {
|
||||
final movies = await getAllMovies();
|
||||
final genres = <String>{};
|
||||
for (final movie in movies) {
|
||||
genres.addAll(movie.genres);
|
||||
}
|
||||
return genres.toList()..sort();
|
||||
}
|
||||
});
|
||||
|
||||
// 一次性获取所有元数据(导演、编剧、演员、类型)
|
||||
Future<Map<String, List<String>>> getAllMetadata() => _wrap('getAllMetadata', () async {
|
||||
final movies = await getAllMovies();
|
||||
final directors = <String>{};
|
||||
final writers = <String>{};
|
||||
final actors = <String>{};
|
||||
final genres = <String>{};
|
||||
for (final movie in movies) {
|
||||
directors.addAll(movie.directors);
|
||||
writers.addAll(movie.writers);
|
||||
actors.addAll(movie.actors);
|
||||
genres.addAll(movie.genres);
|
||||
}
|
||||
return {
|
||||
'directors': directors.toList()..sort(),
|
||||
'writers': writers.toList()..sort(),
|
||||
'actors': actors.toList()..sort(),
|
||||
'genres': genres.toList()..sort(),
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的影视
|
||||
Future<List<Movie>> getDeletedMovies() async {
|
||||
Future<List<Movie>> getDeletedMovies() => _wrap('getDeletedMovies', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
@@ -203,12 +222,11 @@ class MovieDao {
|
||||
whereArgs: [1],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Movie.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复已删除的影视
|
||||
Future<int> restoreMovie(String id) async {
|
||||
Future<int> restoreMovie(String id) => _wrap('restoreMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movies',
|
||||
@@ -216,15 +234,15 @@ class MovieDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 彻底删除影视
|
||||
Future<int> permanentDeleteMovie(String id) async {
|
||||
Future<int> permanentDeleteMovie(String id) => _wrap('permanentDeleteMovie', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'movies',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
||||
class MoviePosterDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[MoviePosterDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取影视的所有海报
|
||||
Future<List<MoviePoster>> getPostersByMovieId(String movieId) async {
|
||||
Future<List<MoviePoster>> getPostersByMovieId(String movieId) => _wrap('getPostersByMovieId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_posters',
|
||||
@@ -15,31 +24,29 @@ class MoviePosterDao {
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => MoviePoster.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
/// 根据ID获取海报
|
||||
Future<MoviePoster?> getPosterById(String id) async {
|
||||
Future<MoviePoster?> getPosterById(String id) => _wrap('getPosterById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_posters',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return MoviePoster.fromJson(maps.first);
|
||||
}
|
||||
});
|
||||
|
||||
/// 添加海报
|
||||
Future<int> insertPoster(MoviePoster poster) async {
|
||||
Future<int> insertPoster(MoviePoster poster) => _wrap('insertPoster', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movie_posters', poster.toJson());
|
||||
}
|
||||
});
|
||||
|
||||
/// 软删除海报
|
||||
Future<int> deletePoster(String id) async {
|
||||
Future<int> deletePoster(String id) => _wrap('deletePoster', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movie_posters',
|
||||
@@ -47,15 +54,15 @@ class MoviePosterDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 获取影视的海报数量
|
||||
Future<int> getPosterCount(String movieId) async {
|
||||
Future<int> getPosterCount(String movieId) => _wrap('getPosterCount', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM movie_posters WHERE movie_id = ? AND is_deleted = 0',
|
||||
[movieId],
|
||||
);
|
||||
return result.first['count'] as int;
|
||||
}
|
||||
return result.first['count'] as int? ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
||||
class MovieReviewDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[MovieReviewDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取影视的所有影评
|
||||
Future<List<MovieReview>> getReviewsByMovieId(String movieId) async {
|
||||
Future<List<MovieReview>> getReviewsByMovieId(String movieId) => _wrap('getReviewsByMovieId', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
@@ -15,31 +24,29 @@ class MovieReviewDao {
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
/// 根据ID获取影评
|
||||
Future<MovieReview?> getReviewById(String id) async {
|
||||
Future<MovieReview?> getReviewById(String id) => _wrap('getReviewById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
where: 'id = ? AND is_deleted = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return MovieReview.fromJson(maps.first);
|
||||
}
|
||||
});
|
||||
|
||||
/// 添加影评
|
||||
Future<int> insertReview(MovieReview review) async {
|
||||
Future<int> insertReview(MovieReview review) => _wrap('insertReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movie_reviews', review.toJson());
|
||||
}
|
||||
});
|
||||
|
||||
/// 更新影评
|
||||
Future<int> updateReview(MovieReview review) async {
|
||||
Future<int> updateReview(MovieReview review) => _wrap('updateReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movie_reviews',
|
||||
@@ -47,10 +54,10 @@ class MovieReviewDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [review.id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 软删除影评
|
||||
Future<int> deleteReview(String id) async {
|
||||
Future<int> deleteReview(String id) => _wrap('deleteReview', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movie_reviews',
|
||||
@@ -58,20 +65,20 @@ class MovieReviewDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// 获取影视的影评数量
|
||||
Future<int> getReviewCount(String movieId) async {
|
||||
Future<int> getReviewCount(String movieId) => _wrap('getReviewCount', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final result = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM movie_reviews WHERE movie_id = ? AND is_deleted = 0',
|
||||
[movieId],
|
||||
);
|
||||
return result.first['count'] as int;
|
||||
}
|
||||
return result.first['count'] as int? ?? 0;
|
||||
});
|
||||
|
||||
/// 获取短评列表
|
||||
Future<List<MovieReview>> getShortReviews(String movieId) async {
|
||||
Future<List<MovieReview>> getShortReviews(String movieId) => _wrap('getShortReviews', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
@@ -79,12 +86,11 @@ class MovieReviewDao {
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
/// 获取长评列表
|
||||
Future<List<MovieReview>> getLongReviews(String movieId) async {
|
||||
Future<List<MovieReview>> getLongReviews(String movieId) => _wrap('getLongReviews', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movie_reviews',
|
||||
@@ -92,7 +98,6 @@ class MovieReviewDao {
|
||||
whereArgs: [movieId],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => MovieReview.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../models/data_models.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
@@ -6,8 +6,17 @@ import '../database_helper.dart';
|
||||
class NoteDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[NoteDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有未删除的笔记
|
||||
Future<List<Note>> getAllNotes() async {
|
||||
Future<List<Note>> getAllNotes() => _wrap('getAllNotes', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
@@ -15,39 +24,37 @@ class NoteDao {
|
||||
whereArgs: [0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 分页查询笔记
|
||||
Future<List<Note>> getNotesPaged({int limit = 20, int offset = 0}) async {
|
||||
Future<List<Note>> getNotesPaged({int limit = 20, int offset = 0}) => _wrap('getNotesPaged', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query('notes', where: 'is_deleted = 0',
|
||||
orderBy: 'created_at DESC', limit: limit, offset: offset);
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据ID获取笔记
|
||||
Future<Note?> getNoteById(String id) async {
|
||||
Future<Note?> getNoteById(String id) => _wrap('getNoteById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
where: 'id = ? AND is_deleted = ?',
|
||||
whereArgs: [id, 0],
|
||||
);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return Note.fromJson(maps.first);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加笔记
|
||||
Future<int> insertNote(Note note) async {
|
||||
Future<int> insertNote(Note note) => _wrap('insertNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('notes', note.toJson());
|
||||
}
|
||||
});
|
||||
|
||||
// 更新笔记
|
||||
Future<int> updateNote(Note note) async {
|
||||
Future<int> updateNote(Note note) => _wrap('updateNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
@@ -55,10 +62,10 @@ class NoteDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [note.id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 软删除笔记
|
||||
Future<int> deleteNote(String id) async {
|
||||
Future<int> deleteNote(String id) => _wrap('deleteNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
@@ -66,12 +73,12 @@ class NoteDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 回收站相关方法 ==========
|
||||
|
||||
// 获取已删除的笔记
|
||||
Future<List<Note>> getDeletedNotes() async {
|
||||
Future<List<Note>> getDeletedNotes() => _wrap('getDeletedNotes', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
@@ -79,12 +86,11 @@ class NoteDao {
|
||||
whereArgs: [1],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复已删除的笔记
|
||||
Future<int> restoreNote(String id) async {
|
||||
Future<int> restoreNote(String id) => _wrap('restoreNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'notes',
|
||||
@@ -92,20 +98,20 @@ class NoteDao {
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 彻底删除笔记
|
||||
Future<int> permanentDeleteNote(String id) async {
|
||||
Future<int> permanentDeleteNote(String id) => _wrap('permanentDeleteNote', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'notes',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索笔记
|
||||
Future<List<Note>> searchNotes(String query) async {
|
||||
Future<List<Note>> searchNotes(String query) => _wrap('searchNotes', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
@@ -113,12 +119,11 @@ class NoteDao {
|
||||
whereArgs: ['%$query%', '%$query%', '%$query%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
|
||||
// 根据标签筛选
|
||||
Future<List<Note>> getNotesByTag(String tag) async {
|
||||
Future<List<Note>> getNotesByTag(String tag) => _wrap('getNotesByTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
@@ -126,7 +131,6 @@ class NoteDao {
|
||||
whereArgs: ['%$tag%', 0],
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -402,18 +403,8 @@ class ServerSyncService {
|
||||
debugPrint('[Sync] 数据库下载响应: ${dbResp.statusCode} size=${dbResp.bodyBytes.length}');
|
||||
if (dbResp.statusCode != 200) return false;
|
||||
|
||||
final dbPath = await DatabaseHelper.instance.databasePath;
|
||||
if (dbPath != null) {
|
||||
final dbFile = File(dbPath);
|
||||
// 先关掉旧连接,删除旧文件,再写入新数据库
|
||||
await DatabaseHelper.instance.close();
|
||||
if (await dbFile.exists()) await dbFile.delete();
|
||||
await dbFile.writeAsBytes(dbResp.bodyBytes);
|
||||
// 设置正确的版本号
|
||||
final db = await openDatabase(dbPath, version: 14);
|
||||
await db.close();
|
||||
debugPrint('[Sync] 数据库已写入: $dbPath');
|
||||
}
|
||||
await DatabaseHelper.instance.reopenDatabaseFromBytes(dbResp.bodyBytes);
|
||||
debugPrint('[Sync] 数据库已重写并重新打开');
|
||||
|
||||
final images = (info['images'] as List<dynamic>?)
|
||||
?.map((e) => e is Map ? {'name': e['name'] as String, 'rel_path': e['rel_path'] as String} : null)
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../database_helper.dart';
|
||||
|
||||
class TagDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
debugPrint('[TagDao] $op error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定类型的所有标签,按名称排序
|
||||
Future<List<Map<String, dynamic>>> getTagsByType(String type) async {
|
||||
Future<List<Map<String, dynamic>>> getTagsByType(String type) => _wrap('getTagsByType', () async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.query('tags',
|
||||
where: 'type = ?',
|
||||
whereArgs: [type],
|
||||
orderBy: 'name ASC');
|
||||
}
|
||||
});
|
||||
|
||||
/// 根据ID获取标签
|
||||
Future<Map<String, dynamic>?> getTagById(String id) async {
|
||||
Future<Map<String, dynamic>?> getTagById(String id) => _wrap('getTagById', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final results = await db.query('tags', where: 'id = ?', whereArgs: [id]);
|
||||
return results.isNotEmpty ? results.first : null;
|
||||
}
|
||||
});
|
||||
|
||||
/// 添加标签,返回新标签ID
|
||||
Future<String> addTag(String name, String type) async {
|
||||
Future<String> addTag(String name, String type) => _wrap('addTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
final id = 'tag_${DateTime.now().millisecondsSinceEpoch}';
|
||||
await db.insert('tags', {
|
||||
@@ -31,10 +42,10 @@ class TagDao {
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
return id;
|
||||
}
|
||||
});
|
||||
|
||||
/// 重命名标签,同时级联更新所有关联条目
|
||||
Future<bool> renameTag(String tagId, String newName) async {
|
||||
/// 重命名标签,同时级联更新所有关联条目(事务保护)
|
||||
Future<bool> renameTag(String tagId, String newName) => _wrap('renameTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
|
||||
final tag = await getTagById(tagId);
|
||||
@@ -50,26 +61,27 @@ class TagDao {
|
||||
whereArgs: [newName, type, tagId]);
|
||||
if (existing.isNotEmpty) return false;
|
||||
|
||||
await db.update('tags', {'name': newName},
|
||||
where: 'id = ?', whereArgs: [tagId]);
|
||||
await db.transaction((txn) async {
|
||||
await txn.update('tags', {'name': newName},
|
||||
where: 'id = ?', whereArgs: [tagId]);
|
||||
|
||||
// 级联更新
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeRenameInMovies(oldName, newName);
|
||||
case 'book_genre':
|
||||
await _cascadeRenameInBooks(oldName, newName);
|
||||
case 'note_tag':
|
||||
await _cascadeRenameInNotes(oldName, newName);
|
||||
}
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeRenameInMovies(txn, oldName, newName);
|
||||
case 'book_genre':
|
||||
await _cascadeRenameInBooks(txn, oldName, newName);
|
||||
case 'note_tag':
|
||||
await _cascadeRenameInNotes(txn, oldName, newName);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
/// 删除标签
|
||||
/// 删除标签(事务保护)
|
||||
/// [replacementName] 不为 null 时,先将所有条目中的旧标签替换为新标签,再删除
|
||||
/// [replacementName] 为 null 时,从所有条目中移除该标签
|
||||
Future<void> deleteTag(String tagId, {String? replacementName}) async {
|
||||
Future<void> deleteTag(String tagId, {String? replacementName}) => _wrap('deleteTag', () async {
|
||||
final db = await _dbHelper.database;
|
||||
|
||||
final tag = await getTagById(tagId);
|
||||
@@ -78,137 +90,144 @@ class TagDao {
|
||||
final name = tag['name'] as String;
|
||||
final type = tag['type'] as String;
|
||||
|
||||
if (replacementName != null && replacementName != name) {
|
||||
await _ensureTagExists(replacementName, type);
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeRenameInMovies(name, replacementName);
|
||||
case 'book_genre':
|
||||
await _cascadeRenameInBooks(name, replacementName);
|
||||
case 'note_tag':
|
||||
await _cascadeRenameInNotes(name, replacementName);
|
||||
await db.transaction((txn) async {
|
||||
if (replacementName != null && replacementName != name) {
|
||||
await _ensureTagExists(txn, replacementName, type);
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeRenameInMovies(txn, name, replacementName);
|
||||
case 'book_genre':
|
||||
await _cascadeRenameInBooks(txn, name, replacementName);
|
||||
case 'note_tag':
|
||||
await _cascadeRenameInNotes(txn, name, replacementName);
|
||||
}
|
||||
} else if (replacementName == null) {
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeDeleteFromMovies(txn, name);
|
||||
case 'book_genre':
|
||||
await _cascadeDeleteFromBooks(txn, name);
|
||||
case 'note_tag':
|
||||
await _cascadeDeleteFromNotes(txn, name);
|
||||
}
|
||||
}
|
||||
} else if (replacementName == null) {
|
||||
switch (type) {
|
||||
case 'movie_genre':
|
||||
await _cascadeDeleteFromMovies(name);
|
||||
case 'book_genre':
|
||||
await _cascadeDeleteFromBooks(name);
|
||||
case 'note_tag':
|
||||
await _cascadeDeleteFromNotes(name);
|
||||
}
|
||||
}
|
||||
|
||||
await db.delete('tags', where: 'id = ?', whereArgs: [tagId]);
|
||||
}
|
||||
await txn.delete('tags', where: 'id = ?', whereArgs: [tagId]);
|
||||
});
|
||||
});
|
||||
|
||||
/// 仅删除标签本身,不级联影响已有条目(标签名保留在条目上)
|
||||
Future<void> deleteTagOnly(String tagId) async {
|
||||
Future<void> deleteTagOnly(String tagId) => _wrap('deleteTagOnly', () async {
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete('tags', where: 'id = ?', whereArgs: [tagId]);
|
||||
}
|
||||
});
|
||||
|
||||
/// 确保标签存在(用于替换操作)
|
||||
Future<void> _ensureTagExists(String name, String type) async {
|
||||
final db = await _dbHelper.database;
|
||||
final existing = await db.query('tags',
|
||||
Future<void> _ensureTagExists(Transaction txn, String name, String type) async {
|
||||
final existing = await txn.query('tags',
|
||||
where: 'name = ? AND type = ?', whereArgs: [name, type]);
|
||||
if (existing.isEmpty) {
|
||||
await addTag(name, type);
|
||||
final id = 'tag_${DateTime.now().millisecondsSinceEpoch}';
|
||||
await txn.insert('tags', {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 级联重命名 ======
|
||||
|
||||
Future<void> _cascadeRenameInMovies(String oldName, String newName) async {
|
||||
final db = await _dbHelper.database;
|
||||
final movies = await db.query('movies');
|
||||
Future<void> _cascadeRenameInMovies(Transaction txn, String oldName, String newName) async {
|
||||
final movies = await txn.query('movies',
|
||||
where: "genres LIKE ?", whereArgs: ['%$oldName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in movies) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (genres.contains(oldName) && !genres.contains(newName)) {
|
||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||
await db.update('movies', {
|
||||
'genres': jsonEncode(updated),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
if (!genres.contains(oldName)) continue;
|
||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||
await txn.update('movies', {
|
||||
'genres': jsonEncode(updated),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeRenameInBooks(String oldName, String newName) async {
|
||||
final db = await _dbHelper.database;
|
||||
final books = await db.query('books');
|
||||
Future<void> _cascadeRenameInBooks(Transaction txn, String oldName, String newName) async {
|
||||
final books = await txn.query('books',
|
||||
where: "genres LIKE ?", whereArgs: ['%$oldName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in books) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (genres.contains(oldName) && !genres.contains(newName)) {
|
||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||
await db.update('books', {
|
||||
'genres': jsonEncode(updated),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
if (!genres.contains(oldName)) continue;
|
||||
final updated = genres.map((g) => g == oldName ? newName : g).toList();
|
||||
await txn.update('books', {
|
||||
'genres': jsonEncode(updated),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeRenameInNotes(String oldName, String newName) async {
|
||||
final db = await _dbHelper.database;
|
||||
final notes = await db.query('notes');
|
||||
Future<void> _cascadeRenameInNotes(Transaction txn, String oldName, String newName) async {
|
||||
final notes = await txn.query('notes',
|
||||
where: "tags LIKE ?", whereArgs: ['%$oldName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in notes) {
|
||||
final tags = _parseList(row['tags']);
|
||||
if (tags.contains(oldName) && !tags.contains(newName)) {
|
||||
final updated = tags.map((t) => t == oldName ? newName : t).toList();
|
||||
await db.update('notes', {
|
||||
'tags': jsonEncode(updated),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
if (!tags.contains(oldName)) continue;
|
||||
final updated = tags.map((t) => t == oldName ? newName : t).toList();
|
||||
await txn.update('notes', {
|
||||
'tags': jsonEncode(updated),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 级联删除 ======
|
||||
|
||||
Future<void> _cascadeDeleteFromMovies(String tagName) async {
|
||||
final db = await _dbHelper.database;
|
||||
final movies = await db.query('movies');
|
||||
Future<void> _cascadeDeleteFromMovies(Transaction txn, String tagName) async {
|
||||
final movies = await txn.query('movies',
|
||||
where: "genres LIKE ?", whereArgs: ['%$tagName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in movies) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (genres.contains(tagName)) {
|
||||
genres.removeWhere((g) => g == tagName);
|
||||
await db.update('movies', {
|
||||
'genres': jsonEncode(genres),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
if (!genres.contains(tagName)) continue;
|
||||
genres.removeWhere((g) => g == tagName);
|
||||
await txn.update('movies', {
|
||||
'genres': jsonEncode(genres),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeDeleteFromBooks(String tagName) async {
|
||||
final db = await _dbHelper.database;
|
||||
final books = await db.query('books');
|
||||
Future<void> _cascadeDeleteFromBooks(Transaction txn, String tagName) async {
|
||||
final books = await txn.query('books',
|
||||
where: "genres LIKE ?", whereArgs: ['%$tagName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in books) {
|
||||
final genres = _parseList(row['genres']);
|
||||
if (genres.contains(tagName)) {
|
||||
genres.removeWhere((g) => g == tagName);
|
||||
await db.update('books', {
|
||||
'genres': jsonEncode(genres),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
if (!genres.contains(tagName)) continue;
|
||||
genres.removeWhere((g) => g == tagName);
|
||||
await txn.update('books', {
|
||||
'genres': jsonEncode(genres),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cascadeDeleteFromNotes(String tagName) async {
|
||||
final db = await _dbHelper.database;
|
||||
final notes = await db.query('notes');
|
||||
Future<void> _cascadeDeleteFromNotes(Transaction txn, String tagName) async {
|
||||
final notes = await txn.query('notes',
|
||||
where: "tags LIKE ?", whereArgs: ['%$tagName%']);
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final row in notes) {
|
||||
final tags = _parseList(row['tags']);
|
||||
if (tags.contains(tagName)) {
|
||||
tags.removeWhere((t) => t == tagName);
|
||||
await db.update('notes', {
|
||||
'tags': jsonEncode(tags),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
if (!tags.contains(tagName)) continue;
|
||||
tags.removeWhere((t) => t == tagName);
|
||||
await txn.update('notes', {
|
||||
'tags': jsonEncode(tags),
|
||||
'updated_at': now,
|
||||
}, where: 'id = ?', whereArgs: [row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class AppTheme {
|
||||
fontSize: 18,
|
||||
fontWeight: _semibold,
|
||||
color: _black,
|
||||
letterSpacing: -0.3,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -184,7 +184,7 @@ class AppTheme {
|
||||
fontSize: 32,
|
||||
fontWeight: _semibold,
|
||||
color: _black,
|
||||
letterSpacing: -0.5,
|
||||
letterSpacing: 0,
|
||||
height: 1.2,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
@@ -192,7 +192,7 @@ class AppTheme {
|
||||
fontSize: 24,
|
||||
fontWeight: _semibold,
|
||||
color: _black,
|
||||
letterSpacing: -0.3,
|
||||
letterSpacing: 0,
|
||||
height: 1.3,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
@@ -200,7 +200,7 @@ class AppTheme {
|
||||
fontSize: 20,
|
||||
fontWeight: _semibold,
|
||||
color: _black,
|
||||
letterSpacing: -0.2,
|
||||
letterSpacing: 0,
|
||||
height: 1.4,
|
||||
),
|
||||
// 正文
|
||||
@@ -282,7 +282,7 @@ class AppTheme {
|
||||
fontSize: 18,
|
||||
fontWeight: _semibold,
|
||||
color: _white,
|
||||
letterSpacing: -0.3,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -380,7 +380,7 @@ class AppTheme {
|
||||
fontSize: 32,
|
||||
fontWeight: _semibold,
|
||||
color: _white,
|
||||
letterSpacing: -0.5,
|
||||
letterSpacing: 0,
|
||||
height: 1.2,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
@@ -388,7 +388,7 @@ class AppTheme {
|
||||
fontSize: 24,
|
||||
fontWeight: _semibold,
|
||||
color: _white,
|
||||
letterSpacing: -0.3,
|
||||
letterSpacing: 0,
|
||||
height: 1.3,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
@@ -396,7 +396,7 @@ class AppTheme {
|
||||
fontSize: 20,
|
||||
fontWeight: _semibold,
|
||||
color: _white,
|
||||
letterSpacing: -0.2,
|
||||
letterSpacing: 0,
|
||||
height: 1.4,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
|
||||
@@ -91,6 +91,8 @@ class UserPrefs {
|
||||
|
||||
// ========== 应用图标设置 ==========
|
||||
|
||||
// ========== Markdown 阅读器 ==========
|
||||
|
||||
/// Markdown 阅读器最近选择的目录
|
||||
String? get lastMdFolder => prefs.getString('lastMdFolder');
|
||||
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