generated from dellevin/template
添加wiki
This commit is contained in:
144
CLAUDE.md
Normal file
144
CLAUDE.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# CLAUDE.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)`)
|
||||
79
CLAUDE2.md
Normal file
79
CLAUDE2.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 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)`)
|
||||
94
Wiki/Architecture.md
Normal file
94
Wiki/Architecture.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 整体架构
|
||||
|
||||
## 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Pages (UI) │ lib/pages/
|
||||
│ home_page, movie_tab_page, book_tab_page, │
|
||||
│ note_tab_page, detail/form/share pages... │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Widgets (共享组件) │ lib/widgets/
|
||||
│ list_items, star_rating, drawer, bottom_nav │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Providers (状态管理) │ lib/providers/
|
||||
│ AppProvider │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Utils (业务逻辑) │ lib/utils/
|
||||
│ DAO层 │ 同步服务 │ 图片管理 │ 主题 │ 偏好设置 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Models (数据模型) │ lib/models/
|
||||
│ Movie │ Book │ Note │ Reviews │ Posters... │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Database (SQLite) │ lib/utils/database_helper.dart
|
||||
│ sqflite │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 目录职责
|
||||
|
||||
### lib/models/
|
||||
数据模型定义,所有模型集中在 `data_models.dart` 中:`Movie`、`Book`、`Note`、`MovieReview`、`BookReview`、`MoviePoster`、`BookExcerpt`。每个模型提供 `fromJson` / `toJson` / `copyWith` 方法。
|
||||
|
||||
### lib/providers/
|
||||
全局状态管理,仅 `AppProvider`(ChangeNotifier)一个文件。管理所有数据列表、UI 状态、CRUD 操作。
|
||||
|
||||
### lib/utils/
|
||||
业务逻辑层:
|
||||
- `database_helper.dart` — SQLite 数据库单例(版本 13)
|
||||
- `movie/`、`book/`、`note/` — 各实体 DAO
|
||||
- `tag/tag_dao.dart` — 标签管理
|
||||
- `sync/` — 同步服务(ServerSyncService、ServerDataService、WebDAV、AutoBackup)
|
||||
- `theme/app_theme.dart` — 主题定义
|
||||
- `user_prefs.dart` — SharedPreferences 包装
|
||||
- `image_path_helper.dart` — 图片路径管理
|
||||
- `app_router.dart` — 路由生成器
|
||||
|
||||
### lib/pages/
|
||||
UI 页面,按功能域组织:
|
||||
- `movies/` — 影视相关(列表、详情、表单、影评、海报墙、分享、豆瓣 WebView)
|
||||
- `book/` — 书籍相关(列表、详情、表单、书评、摘抄、分享)
|
||||
- `note/` — 笔记相关(列表、详情、表单、分享)
|
||||
- `sync/` — 同步页面(备份、云同步、服务端同步、WebDAV)
|
||||
- `markdown_reader/` — Markdown 阅读器
|
||||
|
||||
### lib/widgets/
|
||||
共享可复用 Widget:列表项、星级评分、抽屉菜单、底部导航、骨架屏等。
|
||||
|
||||
## 启动流程
|
||||
|
||||
`main.dart` 中的初始化顺序:
|
||||
|
||||
```
|
||||
1. WidgetsFlutterBinding.ensureInitialized()
|
||||
2. UserPrefs.init() ← SharedPreferences 初始化
|
||||
3. AppProvider() ← 创建全局状态
|
||||
4. runApp(MyApp) ← 启动 UI
|
||||
5. (异步) _validateSyncOnStartup() ← 校验同步激活码
|
||||
6. (异步) appProvider.initDatabase() ← 加载数据(本地或远程)
|
||||
7. (异步) appProvider.initMainTabIndex() ← 恢复用户默认标签
|
||||
8. (异步) _initAutoBackup() ← 启动自动备份(如已启用)
|
||||
9. (异步) _initUsageStats() ← 启动用户统计上报
|
||||
```
|
||||
|
||||
步骤 5-9 均为 `unawaited`,不阻塞 UI 渲染。
|
||||
|
||||
## 本地/远程双模式
|
||||
|
||||
`AppProvider._useRemote` 决定数据来源:
|
||||
|
||||
```dart
|
||||
bool get _useRemote {
|
||||
final prefs = UserPrefs();
|
||||
return prefs.syncEnabled &&
|
||||
prefs.syncServerUrl.isNotEmpty &&
|
||||
prefs.syncActivationCode.isNotEmpty &&
|
||||
ServerDataService.instance.isAvailable;
|
||||
}
|
||||
```
|
||||
|
||||
所有 DAO 方法(增删改查)在 `AppProvider` 中都有双路径:
|
||||
- `_useRemote == true` → 调用 `ServerDataService`(HTTP API)
|
||||
- `_useRemote == false` → 调用本地 DAO(SQLite)
|
||||
|
||||
[返回首页](Home.md)
|
||||
81
Wiki/Data-Access-Layer.md
Normal file
81
Wiki/Data-Access-Layer.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 数据访问层
|
||||
|
||||
## DAO 模式
|
||||
|
||||
每个实体有独立的 DAO 类,封装 SQLite CRUD 操作:
|
||||
|
||||
| DAO | 文件 | 实体 |
|
||||
|-----|------|------|
|
||||
| `MovieDao` | `lib/utils/movie/movie_dao.dart` | Movie |
|
||||
| `BookDao` | `lib/utils/book/book_dao.dart` | Book |
|
||||
| `NoteDao` | `lib/utils/note/note_dao.dart` | Note |
|
||||
| `MovieReviewDao` | `lib/utils/movie/movie_review_dao.dart` | MovieReview |
|
||||
| `MoviePosterDao` | `lib/utils/movie/movie_poster_dao.dart` | MoviePoster |
|
||||
| `BookReviewDao` | `lib/utils/book/book_review_dao.dart` | BookReview |
|
||||
| `BookExcerptDao` | `lib/utils/book/book_excerpt_dao.dart` | BookExcerpt |
|
||||
| `TagDao` | `lib/utils/tag/tag_dao.dart` | Tag |
|
||||
|
||||
## DatabaseHelper
|
||||
|
||||
`lib/utils/database_helper.dart` — SQLite 数据库单例管理器。
|
||||
|
||||
```dart
|
||||
class DatabaseHelper {
|
||||
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||
static Database? _database;
|
||||
|
||||
Future<Database> get database; // 获取数据库实例(懒初始化)
|
||||
Future<void> reopenDatabase(); // 关闭并重新打开(WebDAV 同步后调用)
|
||||
Future<void> close(); // 关闭数据库
|
||||
}
|
||||
```
|
||||
|
||||
- 数据库文件名:`mooknote.db`
|
||||
- 当前版本:13
|
||||
- 初始化时执行 `_createDB`,版本不匹配时执行 `_onUpgrade` 迁移链
|
||||
|
||||
## DAO 通用方法
|
||||
|
||||
以 `MovieDao` 为例:
|
||||
|
||||
```dart
|
||||
getAllMovies() // 获取所有未删除记录
|
||||
getMoviesPaged({status, limit, offset}) // 分页查询
|
||||
getMoviesByStatus(status) // 按状态筛选
|
||||
getMoviesByDirector(director) // 按导演筛选
|
||||
getMoviesByActor(actor) // 按演员筛选
|
||||
insertMovie(movie) // 插入
|
||||
updateMovie(movie) // 更新
|
||||
deleteMovie(id) // 软删除
|
||||
getDeletedMovies() // 获取已删除记录
|
||||
restoreMovie(id) // 恢复
|
||||
permanentDeleteMovie(id) // 彻底删除
|
||||
```
|
||||
|
||||
## 软删除机制
|
||||
|
||||
所有实体使用 `is_deleted` 字段实现软删除:
|
||||
|
||||
```
|
||||
删除操作 → UPDATE SET is_deleted = 1 WHERE id = ?
|
||||
查询操作 → WHERE is_deleted = 0
|
||||
恢复操作 → UPDATE SET is_deleted = 0 WHERE id = ?
|
||||
彻底删除 → DELETE FROM table WHERE id = ?
|
||||
```
|
||||
|
||||
回收站页面(`lib/pages/recycle_bin_page.dart`)展示所有 `is_deleted = 1` 的记录,支持恢复和彻底删除。
|
||||
|
||||
## TagDao
|
||||
|
||||
标签 DAO 管理 `tags` 表,支持:
|
||||
|
||||
- `getTagsByType(type)` — 获取某类型所有标签
|
||||
- `addTag(name, type)` — 添加标签(UNIQUE 约束防重复)
|
||||
- `renameTag(tagId, newName)` — 重命名标签并级联更新关联条目
|
||||
- `deleteTag(tagId, {replacementName})` — 删除标签,可选替换关联条目中的标签名
|
||||
- `deleteTagOnly(tagId)` — 仅删除标签记录,不修改关联条目
|
||||
- `getTagById(tagId)` — 获取单个标签
|
||||
|
||||
标签级联逻辑:重命名或删除标签时,会遍历关联实体表(movies.genres / books.genres / notes.tags),将旧标签名替换为新名称或移除。
|
||||
|
||||
[返回首页](Home.md)
|
||||
203
Wiki/Data-Models.md
Normal file
203
Wiki/Data-Models.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 数据模型
|
||||
|
||||
所有数据模型定义在 `lib/models/data_models.dart` 中。
|
||||
|
||||
## 模型总览
|
||||
|
||||
| 模型 | 说明 | 关键字段 |
|
||||
|------|------|---------|
|
||||
| `Movie` | 影视条目 | title, posterPath, directors, actors, genres, rating, status |
|
||||
| `Book` | 书籍条目 | title, coverPath, authors, publisher, genres, rating, status, isbn |
|
||||
| `Note` | 笔记 | title, content, contentType, tags, images |
|
||||
| `MovieReview` | 影评 | movieId, content, reviewer, source, reviewType (1=短评, 2=长评) |
|
||||
| `BookReview` | 书评 | bookId, content, reviewer, source, reviewType |
|
||||
| `MoviePoster` | 影视海报 | movieId, posterPath |
|
||||
| `BookExcerpt` | 书籍摘抄 | bookId, chapter, content, comment |
|
||||
|
||||
## 公共字段
|
||||
|
||||
所有实体模型都包含:
|
||||
- `id` — UUID 字符串(创建时生成)
|
||||
- `isDeleted` — 软删除标记(0/1)
|
||||
- `createdAt` / `updatedAt` — ISO 8601 时间戳(UTC)
|
||||
|
||||
## copyWith 模式
|
||||
|
||||
模型使用 `_CopyWithNullSentinel` 区分"未传参"和"传了 null":
|
||||
|
||||
```dart
|
||||
class _CopyWithNullSentinel {
|
||||
const _CopyWithNullSentinel();
|
||||
}
|
||||
const _copyWithNull = _CopyWithNullSentinel();
|
||||
|
||||
// 用法示例
|
||||
Movie copyWith({
|
||||
Object? posterPath = _copyWithNull, // 未传参时保留原值
|
||||
Object? summary = _copyWithNull, // 传 null 时清除值
|
||||
}) { ... }
|
||||
```
|
||||
|
||||
这样 `movie.copyWith(title: '新标题')` 不会意外清空 `posterPath`。
|
||||
|
||||
## JSON 编码列表字段
|
||||
|
||||
`directors`、`actors`、`genres`、`tags` 等列表字段在 SQLite 中存储为 JSON 字符串:
|
||||
|
||||
```dart
|
||||
// 序列化
|
||||
'genres': jsonEncode(['科幻', '动作'])
|
||||
|
||||
// 反序列化
|
||||
static List<String> parseStringList(dynamic data) {
|
||||
if (data == null) return [];
|
||||
if (data is List) return data.map((e) => e.toString()).toList();
|
||||
if (data is String) {
|
||||
final decoded = jsonDecode(data);
|
||||
if (decoded is List) return decoded.map((e) => e.toString()).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
## 状态值
|
||||
|
||||
### 影视状态 (Movie.status)
|
||||
| 值 | 含义 |
|
||||
|----|------|
|
||||
| `want_to_watch` | 想看 |
|
||||
| `watching` | 在看 |
|
||||
| `watched` | 已看 |
|
||||
|
||||
### 书籍状态 (Book.status)
|
||||
| 值 | 含义 |
|
||||
|----|------|
|
||||
| `want_to_read` | 想读 |
|
||||
| `reading` | 在读 |
|
||||
| `read` | 已读 |
|
||||
|
||||
## SQLite 表结构
|
||||
|
||||
数据库文件:`mooknote.db`,当前版本:13
|
||||
|
||||
### movies 表
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT PK | UUID |
|
||||
| title | TEXT NOT NULL | 标题 |
|
||||
| poster_path | TEXT | 海报本地路径 |
|
||||
| release_date | TEXT | 上映日期 |
|
||||
| directors | TEXT | JSON 数组 |
|
||||
| writers | TEXT | JSON 数组 |
|
||||
| actors | TEXT | JSON 数组 |
|
||||
| genres | TEXT | JSON 数组 |
|
||||
| alternate_titles | TEXT | JSON 数组 |
|
||||
| summary | TEXT | 剧情简介 |
|
||||
| rating | REAL | 评分 1-10 |
|
||||
| status | TEXT NOT NULL | 状态 |
|
||||
| watch_date | TEXT | 观看日期 |
|
||||
| created_at | TEXT NOT NULL | 创建时间 |
|
||||
| updated_at | TEXT NOT NULL | 更新时间 |
|
||||
| is_deleted | INTEGER DEFAULT 0 | 软删除 |
|
||||
|
||||
### books 表
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT PK | UUID |
|
||||
| title | TEXT NOT NULL | 标题 |
|
||||
| cover_path | TEXT | 封面本地路径 |
|
||||
| authors | TEXT | JSON 数组 |
|
||||
| alternate_titles | TEXT | JSON 数组 |
|
||||
| publisher | TEXT | 出版社 |
|
||||
| genres | TEXT | JSON 数组 |
|
||||
| summary | TEXT | 简介 |
|
||||
| rating | REAL | 评分 1-10 |
|
||||
| status | TEXT NOT NULL | 状态 |
|
||||
| isbn | TEXT | ISBN |
|
||||
| publish_date | TEXT | 出版日期 |
|
||||
| created_at | TEXT NOT NULL | 创建时间 |
|
||||
| updated_at | TEXT NOT NULL | 更新时间 |
|
||||
| is_deleted | INTEGER DEFAULT 0 | 软删除 |
|
||||
|
||||
### notes 表
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT PK | UUID |
|
||||
| title | TEXT DEFAULT '' | 标题 |
|
||||
| content | TEXT NOT NULL | 内容 |
|
||||
| content_type | TEXT DEFAULT 'markdown' | 内容类型 |
|
||||
| tags | TEXT | JSON 数组 |
|
||||
| images | TEXT | JSON 数组(图片路径列表)|
|
||||
| created_at | TEXT NOT NULL | 创建时间 |
|
||||
| updated_at | TEXT NOT NULL | 更新时间 |
|
||||
| is_deleted | INTEGER DEFAULT 0 | 软删除 |
|
||||
|
||||
### movie_reviews 表
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT PK | UUID |
|
||||
| movie_id | TEXT NOT NULL FK | 关联影视 ID |
|
||||
| content | TEXT NOT NULL | 评论内容 |
|
||||
| reviewer | TEXT | 评论者 |
|
||||
| source | TEXT | 来源 |
|
||||
| review_type | INTEGER DEFAULT 1 | 1=短评, 2=长评 |
|
||||
| is_deleted | INTEGER DEFAULT 0 | 软删除 |
|
||||
| created_at / updated_at | TEXT | 时间戳 |
|
||||
|
||||
### book_reviews 表(结构同 movie_reviews,外键为 book_id)
|
||||
|
||||
### movie_posters 表
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT PK | UUID |
|
||||
| movie_id | TEXT NOT NULL FK | 关联影视 ID |
|
||||
| poster_path | TEXT NOT NULL | 海报本地路径 |
|
||||
| is_deleted | INTEGER DEFAULT 0 | 软删除 |
|
||||
| created_at | TEXT | 创建时间 |
|
||||
|
||||
### book_excerpts 表
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT PK | UUID |
|
||||
| book_id | TEXT NOT NULL FK | 关联书籍 ID |
|
||||
| chapter | TEXT | 章节 |
|
||||
| content | TEXT NOT NULL | 摘抄内容 |
|
||||
| comment | TEXT | 感悟评论 |
|
||||
| is_deleted | INTEGER DEFAULT 0 | 软删除 |
|
||||
| created_at / updated_at | TEXT | 时间戳 |
|
||||
|
||||
### tags 表
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT PK | UUID |
|
||||
| name | TEXT NOT NULL | 标签名 |
|
||||
| type | TEXT NOT NULL | movie_genre / book_genre / note_tag |
|
||||
| created_at | TEXT NOT NULL | 创建时间 |
|
||||
| UNIQUE | (name, type) | 同类型标签名唯一 |
|
||||
|
||||
## 数据库迁移链
|
||||
|
||||
| 版本 | 变更 |
|
||||
|------|------|
|
||||
| v1 | 初始版本 |
|
||||
| v2 | 升级 movies 表结构(添加 directors, writers, actors, genres 等字段)|
|
||||
| v3 | 升级 books 表结构(拆分 author → authors, 添加 alternateTitles, publisher, genres)|
|
||||
| v4 | 重建 notes 表(添加 content_type 字段)|
|
||||
| v5 | 创建 movie_reviews 和 movie_posters 表 |
|
||||
| v6 | 为 notes 表添加 is_deleted 字段 |
|
||||
| v7 | 创建 book_reviews 和 book_excerpts 表 |
|
||||
| v8 | 确保 book_reviews/book_excerpts 存在(兼容修复)|
|
||||
| v9 | 为 notes 表添加 images 字段 |
|
||||
| v10 | 为 movies 表添加 watch_date 字段 |
|
||||
| v11 | 为 books 表添加 isbn 和 publish_date 字段 |
|
||||
| v12 | 确保 notes 表有 title 列 |
|
||||
| v13 | 创建 tags 表并回填已有数据 |
|
||||
|
||||
[返回首页](Home.md)
|
||||
102
Wiki/Getting-Started.md
Normal file
102
Wiki/Getting-Started.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# 快速开始
|
||||
|
||||
## 环境要求
|
||||
|
||||
- **Flutter SDK** 3.5.0+
|
||||
- **Android Studio**(含 Android SDK Platform API 33+)
|
||||
- **Android SDK Build-Tools**
|
||||
- Android 真机(USB 调试)或模拟器
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
cd mooknote
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
如果网络问题导致失败,使用国内镜像:
|
||||
|
||||
```powershell
|
||||
$env:PUB_HOSTED_URL="https://pub.flutter-io.cn"
|
||||
$env:FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
或设置代理:
|
||||
|
||||
```powershell
|
||||
$env:HTTP_PROXY="http://127.0.0.1:10808"
|
||||
$env:HTTPS_PROXY="http://127.0.0.1:10808"
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### 3. 连接设备
|
||||
|
||||
- 连接 Android 真机(需开启 USB 调试)
|
||||
- 或启动 Android 模拟器
|
||||
|
||||
### 4. 运行应用
|
||||
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
flutter pub get
|
||||
|
||||
# 运行应用
|
||||
flutter run
|
||||
|
||||
# 构建 release APK
|
||||
flutter build apk --release
|
||||
|
||||
# 构建 App Bundle(推荐用于 Google Play)
|
||||
flutter build appbundle --release
|
||||
|
||||
# 清理构建缓存
|
||||
flutter clean
|
||||
|
||||
# 静态分析
|
||||
flutter analyze
|
||||
```
|
||||
|
||||
## 服务端部署(可选)
|
||||
|
||||
服务端用于用户统计和数据同步,位于 `server/` 目录。
|
||||
|
||||
```bash
|
||||
cd server
|
||||
pip install -r requirements.txt
|
||||
python app.py
|
||||
```
|
||||
|
||||
默认监听 `0.0.0.0:27047`,可通过环境变量 `PORT` 修改端口。
|
||||
|
||||
生产环境推荐 gunicorn:
|
||||
|
||||
```bash
|
||||
pip install gunicorn
|
||||
gunicorn -w 2 -b 0.0.0.0:27047 app:app
|
||||
```
|
||||
|
||||
## 用户统计服务器配置
|
||||
|
||||
在 `lib/utils/usage_stats_service.dart` 中配置统计服务器地址:
|
||||
|
||||
```dart
|
||||
static String serverUrl = 'http://192.168.31.48:5000';
|
||||
```
|
||||
|
||||
置空则禁用用户统计功能。
|
||||
|
||||
[返回首页](Home.md)
|
||||
38
Wiki/Home.md
Normal file
38
Wiki/Home.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# MookNote Wiki
|
||||
|
||||
极简风格的观影阅读笔记应用,基于 Flutter 框架开发。
|
||||
|
||||
## 功能概览
|
||||
|
||||
- **影视管理** — 增删改查、影评、海报墙、豆瓣链接爬取
|
||||
- **书籍管理** — 增删改查、书评、摘抄
|
||||
- **笔记管理** — Markdown 笔记、标签、图片
|
||||
- **数据同步** — 服务端实时同步、WebDAV 同步、自动本地备份
|
||||
- **回收站** — 软删除 + 恢复/彻底删除
|
||||
- **标签管理** — 影视类型、书籍类型、笔记标签统一管理
|
||||
- **统计分析** — 观影/阅读数据统计
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 框架 | Flutter 3.5+, Dart |
|
||||
| 状态管理 | Provider (ChangeNotifier) |
|
||||
| 本地存储 | SQLite (sqflite) |
|
||||
| 远程同步 | HTTP API (http 包) |
|
||||
| 后端 | Python Flask |
|
||||
| 主题 | Material 3, 极简黑白灰 |
|
||||
|
||||
## Wiki 目录
|
||||
|
||||
- [快速开始](Getting-Started.md) — 环境配置、安装与运行
|
||||
- [整体架构](Architecture.md) — 分层架构、目录结构、启动流程
|
||||
- [数据模型](Data-Models.md) — 模型定义、数据库表结构、迁移链
|
||||
- [状态管理](State-Management.md) — AppProvider、Tab 状态、分页加载
|
||||
- [数据访问层](Data-Access-Layer.md) — DAO 模式、软删除、回收站
|
||||
- [图片存储](Image-Storage.md) — 图片路径管理、存储结构
|
||||
- [同步系统](Sync-System.md) — 服务端同步、WebDAV、自动备份
|
||||
- [服务端 API](Server-API.md) — Flask 后端、API 端点、激活码机制
|
||||
- [页面与路由](Pages-and-Navigation.md) — 页面结构、路由表、过渡动画
|
||||
- [主题与 UI](Theme-and-UI.md) — 主题配置、共享组件
|
||||
- [用户偏好](User-Preferences.md) — SharedPreferences 配置项
|
||||
84
Wiki/Image-Storage.md
Normal file
84
Wiki/Image-Storage.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 图片存储
|
||||
|
||||
## ImagePathHelper
|
||||
|
||||
`lib/utils/image_path_helper.dart` — 单例图片路径管理器。
|
||||
|
||||
```dart
|
||||
class ImagePathHelper {
|
||||
static final ImagePathHelper instance = ImagePathHelper._init();
|
||||
}
|
||||
```
|
||||
|
||||
## 存储目录结构
|
||||
|
||||
图片存储在应用文档目录下:
|
||||
|
||||
```
|
||||
{appDocumentsDir}/images/
|
||||
├── movies/
|
||||
│ └── {movieId}/
|
||||
│ ├── poster.jpg ← 影视海报
|
||||
│ └── posterimgs/ ← 海报墙图片
|
||||
│ ├── img1.jpg
|
||||
│ └── img2.jpg
|
||||
├── books/
|
||||
│ └── {bookId}/
|
||||
│ └── cover.jpg ← 书籍封面
|
||||
└── notes/
|
||||
└── {noteId}/
|
||||
├── img1.jpg ← 笔记图片
|
||||
└── img2.jpg
|
||||
```
|
||||
|
||||
## 路径方法
|
||||
|
||||
### 影视
|
||||
|
||||
```dart
|
||||
getMovieImagesDir(movieId) → images/movies/{movieId}/
|
||||
getMoviePosterPath(movieId, file) → images/movies/{movieId}/{file}
|
||||
getMoviePosterImgsDir(movieId) → images/movies/{movieId}/posterimgs/
|
||||
getMoviePosterImgPath(movieId, file)→ images/movies/{movieId}/posterimgs/{file}
|
||||
```
|
||||
|
||||
### 书籍
|
||||
|
||||
```dart
|
||||
getBookImagesDir(bookId) → images/books/{bookId}/
|
||||
getBookCoverPath(bookId, file) → images/books/{bookId}/{file}
|
||||
```
|
||||
|
||||
### 笔记
|
||||
|
||||
```dart
|
||||
getNoteImagesDir(noteId) → images/notes/{noteId}/
|
||||
getNoteImagePath(noteId, file) → images/notes/{noteId}/{file}
|
||||
```
|
||||
|
||||
## 文件操作
|
||||
|
||||
```dart
|
||||
ensureDirExists(dirPath) // 确保目录存在
|
||||
moveFile(src, targetDir, fileName) // 移动文件
|
||||
copyFile(src, targetDir, fileName) // 复制文件
|
||||
deleteFile(filePath) // 删除单个文件
|
||||
deleteMovieImages(movieId) // 删除影视所有图片目录
|
||||
deleteBookImages(bookId) // 删除书籍所有图片目录
|
||||
deleteNoteImages(noteId) // 删除笔记所有图片目录
|
||||
```
|
||||
|
||||
## 远程图片同步
|
||||
|
||||
当 `_useRemote == true` 时,CRUD 操作会自动调用 `ServerDataService.uploadLocalImages(paths)` 上传本地图片到服务端:
|
||||
|
||||
```dart
|
||||
// AppProvider 中
|
||||
await _uploadImagesIfRemote([movie.posterPath]);
|
||||
```
|
||||
|
||||
## 图片显示
|
||||
|
||||
`FadeInLocalImage` Widget(`lib/widgets/fade_in_local_image.dart`)处理本地图片的渐入显示。
|
||||
|
||||
[返回首页](Home.md)
|
||||
100
Wiki/Pages-and-Navigation.md
Normal file
100
Wiki/Pages-and-Navigation.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 页面与路由
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 主页框架
|
||||
|
||||
```
|
||||
HomePage
|
||||
├── CustomDrawer (侧边菜单)
|
||||
├── PageView
|
||||
│ ├── MainContentPage (主内容)
|
||||
│ │ ├── MovieTabPage (观影列表, mainTabIndex=0)
|
||||
│ │ ├── BookTabPage (阅读列表, mainTabIndex=1)
|
||||
│ │ └── NoteTabPage (笔记列表, mainTabIndex=2)
|
||||
│ └── ProfilePage (我的, bottomNavIndex=2)
|
||||
└── BottomNavBar (底部导航)
|
||||
```
|
||||
|
||||
底部导航切换 `bottomNavIndex`:
|
||||
- 0 → 主内容页(MainContentPage)
|
||||
- 1 → 新增页(根据当前 mainTabIndex 跳转对应表单)
|
||||
- 2 → 个人页(ProfilePage)
|
||||
|
||||
### 影视模块页面
|
||||
|
||||
| 页面 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| MovieTabPage | `pages/movies/movie_tab_page.dart` | 影视列表(按状态筛选)|
|
||||
| MovieFormPage | `pages/movies/movie_form_page.dart` | 新增/编辑影视 |
|
||||
| MovieDetailPage | `pages/movies/movie_detail_page.dart` | 影视详情 |
|
||||
| MovieReviewsPage | `pages/movies/movie_reviews_page.dart` | 影评列表 |
|
||||
| MovieReviewFormPage | `pages/movies/movie_review_form_page.dart` | 新增/编辑影评 |
|
||||
| MovieReviewDetailPage | `pages/movies/movie_review_detail_page.dart` | 影评详情 |
|
||||
| MoviePostersPage | `pages/movies/movie_posters_page.dart` | 海报墙 |
|
||||
| PosterGalleryPage | `pages/movies/poster_gallery_page.dart` | 海报画廊浏览 |
|
||||
| MovieSharePage | `pages/movies/movie_share_page.dart` | 影视分享 |
|
||||
| DoubanWebViewPage | `pages/movies/douban_webview_page.dart` | 豆瓣 WebView |
|
||||
|
||||
### 书籍模块页面
|
||||
|
||||
| 页面 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| BookTabPage | `pages/book/book_tab_page.dart` | 书籍列表 |
|
||||
| BookFormPage | `pages/book/book_form_page.dart` | 新增/编辑书籍 |
|
||||
| BookDetailPage | `pages/book/book_detail_page.dart` | 书籍详情 |
|
||||
| BookReviewsPage | `pages/book/book_reviews_page.dart` | 书评列表 |
|
||||
| BookReviewFormPage | `pages/book/book_review_form_page.dart` | 新增/编辑书评 |
|
||||
| BookReviewDetailPage | `pages/book/book_review_detail_page.dart` | 书评详情 |
|
||||
| BookExcerptsPage | `pages/book/book_excerpts_page.dart` | 摘抄列表 |
|
||||
| BookExcerptFormPage | `pages/book/book_excerpt_form_page.dart` | 新增/编辑摘抄 |
|
||||
| BookSharePage | `pages/book/book_share_page.dart` | 书籍分享 |
|
||||
|
||||
### 笔记模块页面
|
||||
|
||||
| 页面 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| NoteTabPage | `pages/note/note_tab_page.dart` | 笔记列表 |
|
||||
| NoteFormPage | `pages/note/note_form_page.dart` | 新增/编辑笔记 |
|
||||
| NoteDetailPage | `pages/note/note_detail_page.dart` | 笔记详情 |
|
||||
| NoteSharePage | `pages/note/note_share_page.dart` | 笔记分享 |
|
||||
|
||||
### 其他页面
|
||||
|
||||
| 页面 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| HomePage | `pages/home_page.dart` | 主页框架 |
|
||||
| MainContentPage | `pages/main_content_page.dart` | 主内容区域 |
|
||||
| ProfilePage | `pages/profile_page.dart` | 个人页 |
|
||||
| SearchPage | `pages/search_page.dart` | 全局搜索 |
|
||||
| StatisticsPage | `pages/statistics_page.dart` | 统计分析 |
|
||||
| StrollPage | `pages/stroll_page.dart` | 浏览/发现 |
|
||||
| RecycleBinPage | `pages/recycle_bin_page.dart` | 回收站 |
|
||||
| TagManagementPage | `pages/tag_management_page.dart` | 标签管理 |
|
||||
| AppIconPickerPage | `pages/app_icon_picker_page.dart` | 应用图标选择 |
|
||||
| BackupPage | `pages/sync/backup_page.dart` | 本地备份 |
|
||||
| CloudSyncPage | `pages/sync/cloud_sync_page.dart` | 云同步 |
|
||||
| ServerSyncPage | `pages/sync/server_sync_page.dart` | 服务端同步配置 |
|
||||
| WebDAVSyncPage | `pages/sync/webdav_sync_page.dart` | WebDAV 同步 |
|
||||
| MdReaderTabPage | `pages/markdown_reader/md_reader_tab_page.dart` | Markdown 阅读器目录 |
|
||||
| MdViewerPage | `pages/markdown_reader/md_viewer_page.dart` | Markdown 查看器 |
|
||||
|
||||
## 路由
|
||||
|
||||
`AppRouter`(`lib/utils/app_router.dart`)使用 `onGenerateRoute` 生成路由:
|
||||
|
||||
| 路由名 | 参数 | 目标页面 |
|
||||
|--------|------|---------|
|
||||
| `/movie-form` | `Movie?` 或 `{initialStatus}` | MovieFormPage |
|
||||
| `/book-form` | `Book?` 或 `{initialStatus}` | BookFormPage |
|
||||
| `/note-form` | `Note?` | NoteFormPage |
|
||||
| `/movie-detail` | `Movie` | MovieDetailPage |
|
||||
| `/book-detail` | `Book` | BookDetailPage |
|
||||
| `/note-detail` | `Note` | NoteDetailPage |
|
||||
| `/douban-webview` | `String` (URL) | DoubanWebViewPage |
|
||||
|
||||
## 过渡动画
|
||||
|
||||
所有路由使用 `SlideUpPageRoute`(`lib/utils/slide_up_page_route.dart`),实现从底部滑入的过渡效果。
|
||||
|
||||
[返回首页](Home.md)
|
||||
117
Wiki/Server-API.md
Normal file
117
Wiki/Server-API.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 服务端 API
|
||||
|
||||
## 概述
|
||||
|
||||
服务端为 Python Flask 应用,位于 `server/` 目录。提供用户统计、数据同步和管理后台功能。
|
||||
|
||||
## 模块结构
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `app.py` | 入口,注册所有路由模块 |
|
||||
| `config.py` | 配置(JWT 密钥、数据库路径、备份目录)|
|
||||
| `database.py` | 数据库初始化与管理(SQLite)|
|
||||
| `auth.py` | 认证路由(激活码验证)|
|
||||
| `admin_api.py` | 管理后台 API |
|
||||
| `sync_api.py` | 同步 API(文件上传/下载、心跳)|
|
||||
| `data_api.py` | 数据 CRUD API(影视/书籍/笔记/标签/图片)|
|
||||
| `web_ui.py` | Web 管理界面 |
|
||||
| `static/` | 静态资源 |
|
||||
| `requirements.txt` | Python 依赖 |
|
||||
|
||||
## API 端点
|
||||
|
||||
### 认证
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/activate` | 校验激活码 |
|
||||
|
||||
请求体:`{"code": "激活码", "device_id": "设备标识"}`
|
||||
|
||||
响应:`{"valid": true, "expires_at": "...", "is_permanent": false}`
|
||||
|
||||
### 数据 CRUD
|
||||
|
||||
所有请求需在 body 中附带 `code` 字段。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/data/movies` | 获取影视列表(支持 status/limit/offset)|
|
||||
| POST | `/api/data/movie/save` | 保存影视(新建/更新)|
|
||||
| POST | `/api/data/movie/delete` | 删除影视 |
|
||||
| POST | `/api/data/books` | 获取书籍列表 |
|
||||
| POST | `/api/data/book/save` | 保存书籍 |
|
||||
| POST | `/api/data/book/delete` | 删除书籍 |
|
||||
| POST | `/api/data/notes` | 获取笔记列表 |
|
||||
| POST | `/api/data/note/save` | 保存笔记 |
|
||||
| POST | `/api/data/note/delete` | 删除笔记 |
|
||||
| POST | `/api/data/movie_reviews` | 获取影评 |
|
||||
| POST | `/api/data/movie_review/save` | 保存影评 |
|
||||
| POST | `/api/data/movie_review/delete` | 删除影评 |
|
||||
| POST | `/api/data/movie_posters` | 获取海报 |
|
||||
| POST | `/api/data/movie_poster/save` | 保存海报 |
|
||||
| POST | `/api/data/movie_poster/delete` | 删除海报 |
|
||||
| POST | `/api/data/book_reviews` | 获取书评 |
|
||||
| POST | `/api/data/book_review/save` | 保存书评 |
|
||||
| POST | `/api/data/book_review/delete` | 删除书评 |
|
||||
| POST | `/api/data/book_excerpts` | 获取摘抄 |
|
||||
| POST | `/api/data/book_excerpt/save` | 保存摘抄 |
|
||||
| POST | `/api/data/book_excerpt/delete` | 删除摘抄 |
|
||||
| POST | `/api/data/tags` | 获取标签 |
|
||||
| POST | `/api/data/tag/save` | 保存标签 |
|
||||
| POST | `/api/data/tag/delete` | 删除标签 |
|
||||
|
||||
### 同步
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/heartbeat` | 设备心跳上报 |
|
||||
| POST | `/api/sync/upload` | 上传数据库/图片文件 |
|
||||
| POST | `/api/sync/download` | 下载数据库/图片文件 |
|
||||
|
||||
### 图片
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/data/image/upload` | 上传图片 |
|
||||
| GET | `/api/data/image/{code}/{path}` | 获取图片 |
|
||||
|
||||
## 激活码机制
|
||||
|
||||
每个激活码对应一个独立的 SQLite 数据库:
|
||||
|
||||
```
|
||||
server/backups/{CODE}/
|
||||
├── database/
|
||||
│ └── mooknote.db ← 该激活码的数据
|
||||
├── images/ ← 图片文件
|
||||
└── avatars/ ← 头像文件
|
||||
```
|
||||
|
||||
激活码校验通过 `admin_api.py` 中的 `_verify_code()` 函数完成,支持有效期和永久有效两种模式。
|
||||
|
||||
## 服务端数据库
|
||||
|
||||
服务端自身使用 `stats.db` 存储设备信息和心跳日志:
|
||||
|
||||
| 表 | 说明 |
|
||||
|----|------|
|
||||
| `devices` | 设备注册(device_hash, first_seen, last_seen)|
|
||||
| `heartbeat_logs` | 心跳日志(device_hash, ip, device_type, device_name)|
|
||||
| `changelog` | 更新日志 |
|
||||
|
||||
每个激活码的数据存储在独立的 `backups/{code}/database/mooknote.db` 中,表结构与客户端一致。
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd server
|
||||
python app.py # 开发环境(waitress)
|
||||
# 或
|
||||
gunicorn -w 2 -b 0.0.0.0:27047 app:app # 生产环境
|
||||
```
|
||||
|
||||
默认端口:27047(可通过环境变量 `PORT` 修改)。
|
||||
|
||||
[返回首页](Home.md)
|
||||
111
Wiki/State-Management.md
Normal file
111
Wiki/State-Management.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 状态管理
|
||||
|
||||
## AppProvider
|
||||
|
||||
`AppProvider`(`lib/providers/app_provider.dart`)是应用唯一的 ChangeNotifier,通过 `MultiProvider` 在根节点注入,所有页面通过 `context.watch<AppProvider>()` 或 `context.read<AppProvider>()` 访问。
|
||||
|
||||
## 管理的状态
|
||||
|
||||
### 数据列表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `_movies` | `List<Movie>` | 影视数据 |
|
||||
| `_books` | `List<Book>` | 书籍数据 |
|
||||
| `_notes` | `List<Note>` | 笔记数据 |
|
||||
|
||||
### UI 状态
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `_mainTabIndex` | `int` | 主标签页(0=观影, 1=阅读, 2=笔记)|
|
||||
| `_bottomNavIndex` | `int` | 底部导航(0=主页, 1=新增, 2=我的)|
|
||||
| `_bottomNavVisible` | `bool` | 底部导航栏可见性 |
|
||||
| `_themeMode` | `ThemeMode` | 主题模式 |
|
||||
| `_movieStatusIndex` | `int` | 影视状态筛选(0=已看, 1=想看, 2=在看)|
|
||||
| `_bookStatusIndex` | `int` | 书籍状态筛选(0=读完, 1=在读, 2=准备读)|
|
||||
| `_drawerOpen` | `bool` | 侧边菜单状态 |
|
||||
|
||||
### DAO 实例
|
||||
|
||||
```dart
|
||||
final MovieDao _movieDao = MovieDao();
|
||||
final BookDao _bookDao = BookDao();
|
||||
final NoteDao _noteDao = NoteDao();
|
||||
final MovieReviewDao _reviewDao = MovieReviewDao();
|
||||
final MoviePosterDao _posterDao = MoviePosterDao();
|
||||
final BookReviewDao _bookReviewDao = BookReviewDao();
|
||||
final BookExcerptDao _bookExcerptDao = BookExcerptDao();
|
||||
final TagDao _tagDao = TagDao();
|
||||
```
|
||||
|
||||
## 数据加载
|
||||
|
||||
### 全量加载
|
||||
|
||||
```dart
|
||||
initDatabase() → loadMovies() + loadBooks() + loadNotes()
|
||||
```
|
||||
|
||||
根据 `_useRemote` 决定从本地 SQLite 或远程 API 加载。
|
||||
|
||||
### 分页加载
|
||||
|
||||
分页大小:`_pageSize = 20`
|
||||
|
||||
```dart
|
||||
loadMoviesPaged({String? status, required int offset})
|
||||
loadBooksPaged({String? status, required int offset})
|
||||
loadNotesPaged({required int offset})
|
||||
```
|
||||
|
||||
供列表页触底加载使用。
|
||||
|
||||
## CRUD 操作
|
||||
|
||||
所有 CRUD 方法都走双路径(本地/远程),并自动重新加载数据:
|
||||
|
||||
```dart
|
||||
addMovie(Movie movie) → 本地: _movieDao.insertMovie / 远程: ServerDataService.saveMovie → loadMovies()
|
||||
updateMovie(Movie movie) → 本地: _movieDao.updateMovie / 远程: ServerDataService.saveMovie → loadMovies()
|
||||
removeMovie(String id) → 本地: _movieDao.deleteMovie / 远程: ServerDataService.deleteMovie → loadMovies()
|
||||
```
|
||||
|
||||
Book、Note 同理。远程模式下还会自动上传关联图片。
|
||||
|
||||
## 子实体操作
|
||||
|
||||
影评、海报、书评、摘抄的 CRUD 方法不直接修改内存列表,而是按需查询:
|
||||
|
||||
```dart
|
||||
getMovieReviews(movieId) / addMovieReview(review) / updateMovieReview(review) / removeMovieReview(id)
|
||||
getMoviePosters(movieId) / addMoviePoster(poster) / removeMoviePoster(id)
|
||||
getBookReviews(bookId) / addBookReview(review) / updateBookReview(review) / removeBookReview(id)
|
||||
getBookExcerpts(bookId) / addBookExcerpt(excerpt) / updateBookExcerpt(excerpt) / removeBookExcerpt(id)
|
||||
```
|
||||
|
||||
## 回收站
|
||||
|
||||
```dart
|
||||
getDeletedMovies() / restoreMovie(id) / permanentDeleteMovie(id)
|
||||
getDeletedBooks() / restoreBook(id) / permanentDeleteBook(id)
|
||||
getDeletedNotes() / restoreNote(id) / permanentDeleteNote(id)
|
||||
clearRecycleBin() // 清空所有已删除条目
|
||||
```
|
||||
|
||||
永久删除时会同时清理关联图片目录。
|
||||
|
||||
## 标签管理
|
||||
|
||||
```dart
|
||||
getTags(type) // 获取某类型标签
|
||||
addTag(name, type) // 添加标签
|
||||
renameTag(tagId, newName, type) // 重命名(级联更新关联条目)
|
||||
deleteTag(tagId, type, {replacementName}) // 删除(可替换关联条目的标签名)
|
||||
deleteTagOnly(tagId, type) // 仅删除标签本身,不影响条目
|
||||
syncTagsFromData() // 从现有数据回填标签表
|
||||
```
|
||||
|
||||
标签类型:`movie_genre`、`book_genre`、`note_tag`。
|
||||
|
||||
[返回首页](Home.md)
|
||||
93
Wiki/Sync-System.md
Normal file
93
Wiki/Sync-System.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 同步系统
|
||||
|
||||
MookNote 支持三种数据同步方式。
|
||||
|
||||
## 1. 服务端实时同步
|
||||
|
||||
通过激活码连接远程服务器,数据实时双向同步。
|
||||
|
||||
### 核心服务
|
||||
|
||||
#### ServerDataService (`lib/utils/sync/server_data_service.dart`)
|
||||
|
||||
远程 API 数据操作层,所有方法通过 HTTP POST 调用服务端接口:
|
||||
|
||||
```dart
|
||||
// 影视
|
||||
getMovies({status, limit, offset}) → POST /api/data/movies
|
||||
saveMovie(movie) → POST /api/data/movie/save
|
||||
deleteMovie(id) → POST /api/data/movie/delete
|
||||
|
||||
// 书籍、笔记、影评、海报、书评、摘抄、标签同理
|
||||
```
|
||||
|
||||
每个请求都附带 `code`(激活码)进行身份验证。
|
||||
|
||||
#### ServerSyncService (`lib/utils/sync/server_sync_service.dart`)
|
||||
|
||||
同步协调服务:
|
||||
|
||||
- `checkActivation()` — 校验激活码有效性
|
||||
- `syncWithServer()` — 智能合并本地与服务端数据
|
||||
- `downloadToLocal()` — 从服务端下载数据到本地(关闭同步时调用)
|
||||
|
||||
### 启动时校验流程
|
||||
|
||||
```
|
||||
1. 检查 syncEnabled、syncServerUrl、syncActivationCode 是否已配置
|
||||
2. 调用 /api/activate 校验激活码
|
||||
3. 有效 → 更新有效期信息,继续同步模式
|
||||
4. 无效/过期 → 调用 downloadToLocal() 下载数据 → 关闭同步开关
|
||||
```
|
||||
|
||||
### 双模式切换
|
||||
|
||||
`AppProvider._useRemote` 控制:
|
||||
|
||||
```dart
|
||||
bool get _useRemote {
|
||||
return prefs.syncEnabled &&
|
||||
prefs.syncServerUrl.isNotEmpty &&
|
||||
prefs.syncActivationCode.isNotEmpty &&
|
||||
ServerDataService.instance.isAvailable;
|
||||
}
|
||||
```
|
||||
|
||||
## 2. WebDAV 同步
|
||||
|
||||
通过 WebDAV 协议同步数据库文件和图片。
|
||||
|
||||
相关页面:`lib/pages/sync/webdav_sync_page.dart`
|
||||
服务类:`lib/utils/sync/webdav_service.dart`
|
||||
|
||||
## 3. 自动本地备份
|
||||
|
||||
### AutoBackupService (`lib/utils/sync/auto_backup_service.dart`)
|
||||
|
||||
定时自动备份到设备下载目录:
|
||||
|
||||
- **间隔**:5 分钟
|
||||
- **保留数量**:最近 5 个备份文件
|
||||
- **备份目录**:`Download/mooknote/`(Android)或 `Documents/mooknote/`(iOS)
|
||||
- **文件格式**:`auto_backup_{yyyyMMdd_HHmmss}.zip`
|
||||
|
||||
### BackupService (`lib/utils/sync/backup_service.dart`)
|
||||
|
||||
手动备份与恢复服务,导出数据为 ZIP 文件(包含数据库和图片)。
|
||||
|
||||
相关页面:`lib/pages/sync/backup_page.dart`
|
||||
|
||||
## 同步相关配置
|
||||
|
||||
在 `UserPrefs` 中:
|
||||
|
||||
| 键 | 说明 |
|
||||
|----|------|
|
||||
| `syncServerUrl` | 服务器地址 |
|
||||
| `syncActivationCode` | 激活码 |
|
||||
| `syncExpiresAt` | 激活码有效期 |
|
||||
| `syncIsPermanent` | 是否永久有效 |
|
||||
| `syncEnabled` | 同步开关 |
|
||||
| `syncLastEntryId` | 上次同步的 entry ID |
|
||||
|
||||
[返回首页](Home.md)
|
||||
75
Wiki/Theme-and-UI.md
Normal file
75
Wiki/Theme-and-UI.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 主题与 UI
|
||||
|
||||
## AppTheme
|
||||
|
||||
`lib/utils/theme/app_theme.dart` — 极简主义黑白灰主题配置。
|
||||
|
||||
### 颜色体系
|
||||
|
||||
#### 中性色板
|
||||
|
||||
| 颜色 | 值 | 用途 |
|
||||
|------|------|------|
|
||||
| `_black` | `#1A1A1A` | 主要文字、主色 |
|
||||
| `_darkGray` | `#333333` | 次要文字 |
|
||||
| `_gray` | `#666666` | 辅助文字 |
|
||||
| `_lightGray` | `#999999` | 占位文字 |
|
||||
| `_lighterGray` | `#E5E5E5` | 分割线、边框 |
|
||||
| `_offWhite` | `#F5F5F5` | 背景色 |
|
||||
| `_white` | `#FFFFFF` | 主背景 |
|
||||
|
||||
#### 强调色
|
||||
|
||||
| 颜色 | 值 | 用途 |
|
||||
|------|------|------|
|
||||
| `accent` | `#0066FF` | 关键操作高亮 |
|
||||
| `error` | `#DC2626` | 错误状态 |
|
||||
|
||||
#### 状态颜色
|
||||
|
||||
| 状态 | 颜色 | 说明 |
|
||||
|------|------|------|
|
||||
| `watched` / `readColor` | `#1A1A1A` | 已看/已读(纯黑)|
|
||||
| `watching` / `readingColor` | `#666666` | 在看/在读(中灰)|
|
||||
| `wantToWatch` / `wantToReadColor` | `#999999` | 想看/想读(浅灰)|
|
||||
|
||||
### Material 3 配置
|
||||
|
||||
```dart
|
||||
ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light/dark,
|
||||
scaffoldBackgroundColor: _white/_black,
|
||||
colorScheme: ColorScheme.light/dark(...),
|
||||
fontFamily: 'Inter',
|
||||
)
|
||||
```
|
||||
|
||||
提供 `lightTheme` 和 `darkTheme` 两套主题。
|
||||
|
||||
### 字体
|
||||
|
||||
- 字体族:`Inter`
|
||||
- 字重:`w400`(Regular)、`w500`(Medium)、`w600`(Semibold)
|
||||
|
||||
## 共享 Widget
|
||||
|
||||
| Widget | 文件 | 说明 |
|
||||
|--------|------|------|
|
||||
| `MovieListItem` | `widgets/movie_list_item.dart` | 影视列表项 |
|
||||
| `BookListItem` | `widgets/book_list_item.dart` | 书籍列表项 |
|
||||
| `NoteListItem` | `widgets/note_list_item.dart` | 笔记列表项 |
|
||||
| `MovieStatusBar` | `widgets/movie_status_bar.dart` | 影视状态筛选栏 |
|
||||
| `BookStatusBar` | `widgets/book_status_bar.dart` | 书籍状态筛选栏 |
|
||||
| `AnimatedStarRating` | `widgets/animated_star_rating.dart` | 动画星级评分 |
|
||||
| `BottomNavBar` | `widgets/bottom_nav_bar.dart` | 底部导航栏 |
|
||||
| `CustomDrawer` | `widgets/custom_drawer.dart` | 侧边抽屉菜单 |
|
||||
| `FadeInLocalImage` | `widgets/fade_in_local_image.dart` | 本地图片渐入显示 |
|
||||
| `AppRefreshIndicator` | `widgets/app_refresh_indicator.dart` | 自定义下拉刷新 |
|
||||
| `ShimmerSkeleton` | `widgets/shimmer_skeleton.dart` | 骨架屏加载效果 |
|
||||
|
||||
## 系统 UI
|
||||
|
||||
`MyApp` 在 `main.dart` 中通过 `SystemChrome.setSystemUIOverlayStyle` 控制状态栏和导航栏样式,随主题模式自动切换亮/暗色。
|
||||
|
||||
[返回首页](Home.md)
|
||||
94
Wiki/User-Preferences.md
Normal file
94
Wiki/User-Preferences.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 用户偏好
|
||||
|
||||
## UserPrefs
|
||||
|
||||
`lib/utils/user_prefs.dart` — SharedPreferences 单例包装器。
|
||||
|
||||
```dart
|
||||
class UserPrefs {
|
||||
static final UserPrefs _instance = UserPrefs._internal();
|
||||
factory UserPrefs() => _instance;
|
||||
}
|
||||
```
|
||||
|
||||
使用前必须调用 `UserPrefs.init()`(在 `main.dart` 中完成)。
|
||||
|
||||
## 配置项列表
|
||||
|
||||
### 用户信息
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `nickname` | String | `'Mook'` | 昵称 |
|
||||
| `motto` | String | `'好运不会眷顾一无所有之人。'` | 座右铭 |
|
||||
| `avatarPath` | String? | `null` | 头像本地路径 |
|
||||
|
||||
### 应用设置
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `themeMode` | int | `0` | 0=跟随系统, 1=浅色, 2=深色 |
|
||||
| `showExactReleaseDate` | bool | `true` | 上映日期显示到日/月 |
|
||||
| `isFirstLaunch` | bool | `true` | 是否首次启动 |
|
||||
|
||||
### 主界面显示
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `hideBottomNavOnScroll` | bool | `true` | 滚动时隐藏底部导航 |
|
||||
| `showMovieTab` | bool | `true` | 是否显示观影标签 |
|
||||
| `showBookTab` | bool | `true` | 是否显示阅读标签 |
|
||||
| `showNoteTab` | bool | `true` | 是否显示笔记标签 |
|
||||
| `defaultMainTabIndex` | int | `0` | 默认启动标签(0=影视, 1=阅读, 2=笔记)|
|
||||
|
||||
### 布局样式
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `noteLayoutStyle` | int | `0` | 笔记布局(0=列表, 1=瀑布流, 2=时间线)|
|
||||
| `movieLayoutStyle` | int | `0` | 影视布局(0=海报网格, 1=列表)|
|
||||
| `bookLayoutStyle` | int | `0` | 阅读布局(0=封面网格, 1=列表)|
|
||||
|
||||
### Markdown 阅读器
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `lastMdFolder` | String? | `null` | 最近选择的目录 |
|
||||
| `showEmptyDirs` | bool | `true` | 是否显示空目录 |
|
||||
| `showImageOnlyDirs` | bool | `true` | 是否显示纯图片目录 |
|
||||
|
||||
### 应用图标
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `appIconName` | String | `'app_icon'` | 当前应用图标名称 |
|
||||
|
||||
### 用户统计
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `deviceId` | String | `''` | 匿名设备标识(首次启动自动生成)|
|
||||
|
||||
### 服务端同步
|
||||
|
||||
| 键 | 类型 | 默认值 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `syncServerUrl` | String | `''` | 服务器地址 |
|
||||
| `syncActivationCode` | String | `''` | 激活码 |
|
||||
| `syncExpiresAt` | String | `''` | 激活码有效期 |
|
||||
| `syncIsPermanent` | bool | `false` | 是否永久有效 |
|
||||
| `syncEnabled` | bool | `true` | 实时同步开关 |
|
||||
| `syncLastEntryId` | int | `0` | 上次同步的 entry ID |
|
||||
|
||||
## 主题模式迁移
|
||||
|
||||
`UserPrefs.init()` 中包含旧版 `isDarkMode`(布尔值)到新版 `themeMode`(三态值)的自动迁移:
|
||||
|
||||
```dart
|
||||
if (_prefs!.containsKey('isDarkMode') && !_prefs!.containsKey('themeMode')) {
|
||||
final oldValue = _prefs!.getBool('isDarkMode') ?? false;
|
||||
await _prefs!.setInt('themeMode', oldValue ? 2 : 0);
|
||||
}
|
||||
```
|
||||
|
||||
[返回首页](Home.md)
|
||||
@@ -83,7 +83,8 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
border:
|
||||
Border.all(color: colors.outlineVariant, width: 0.5),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Image.asset(
|
||||
@@ -95,7 +96,9 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
width: 64,
|
||||
height: 64,
|
||||
color: colors.surfaceContainerHighest,
|
||||
child: Icon(Icons.image_not_supported, size: 24, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
child: Icon(Icons.image_not_supported,
|
||||
size: 24,
|
||||
color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -113,7 +116,9 @@ class _AppIconPickerPageState extends State<AppIconPickerPage> {
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle, size: 22, color: colors.primary)
|
||||
else
|
||||
Icon(Icons.radio_button_unchecked, size: 22, color: colors.onSurface.withValues(alpha: 0.2)),
|
||||
Icon(Icons.radio_button_unchecked,
|
||||
size: 22,
|
||||
color: colors.onSurface.withValues(alpha: 0.2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -686,8 +686,9 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
}
|
||||
|
||||
void _navigateToEdit(BuildContext context) {
|
||||
final provider = context.read<AppProvider>();
|
||||
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
|
||||
context.read<AppProvider>().loadBooks();
|
||||
provider.loadBooks();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -692,8 +692,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
}
|
||||
|
||||
void _navigateToEdit(BuildContext context) {
|
||||
final provider = context.read<AppProvider>();
|
||||
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
|
||||
context.read<AppProvider>().loadMovies();
|
||||
provider.loadMovies();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -291,8 +291,9 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
(n) => n.id == widget.note.id,
|
||||
orElse: () => widget.note,
|
||||
);
|
||||
final provider = context.read<AppProvider>();
|
||||
Navigator.pushNamed(context, '/note-form', arguments: currentNote).then((_) async {
|
||||
await context.read<AppProvider>().loadNotes();
|
||||
await provider.loadNotes();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
final provider = context.read<AppProvider>();
|
||||
Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async {
|
||||
await context.read<AppProvider>().loadNotes();
|
||||
await provider.loadNotes();
|
||||
});
|
||||
},
|
||||
onLongPress: () => _showDeleteDialog(context),
|
||||
|
||||
Reference in New Issue
Block a user