diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ed56896 --- /dev/null +++ b/CLAUDE.md @@ -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()` or `context.read()`. + +### 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///` 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)`) diff --git a/CLAUDE2.md b/CLAUDE2.md new file mode 100644 index 0000000..8e59037 --- /dev/null +++ b/CLAUDE2.md @@ -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()` or `context.read()`. + +### 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///` 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)`) diff --git a/Wiki/Architecture.md b/Wiki/Architecture.md new file mode 100644 index 0000000..63820a3 --- /dev/null +++ b/Wiki/Architecture.md @@ -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) diff --git a/Wiki/Data-Access-Layer.md b/Wiki/Data-Access-Layer.md new file mode 100644 index 0000000..3ddd620 --- /dev/null +++ b/Wiki/Data-Access-Layer.md @@ -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 get database; // 获取数据库实例(懒初始化) + Future reopenDatabase(); // 关闭并重新打开(WebDAV 同步后调用) + Future 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) diff --git a/Wiki/Data-Models.md b/Wiki/Data-Models.md new file mode 100644 index 0000000..f8baba3 --- /dev/null +++ b/Wiki/Data-Models.md @@ -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 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) diff --git a/Wiki/Getting-Started.md b/Wiki/Getting-Started.md new file mode 100644 index 0000000..6f6123e --- /dev/null +++ b/Wiki/Getting-Started.md @@ -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) diff --git a/Wiki/Home.md b/Wiki/Home.md new file mode 100644 index 0000000..7f6b2b7 --- /dev/null +++ b/Wiki/Home.md @@ -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 配置项 diff --git a/Wiki/Image-Storage.md b/Wiki/Image-Storage.md new file mode 100644 index 0000000..120301d --- /dev/null +++ b/Wiki/Image-Storage.md @@ -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) diff --git a/Wiki/Pages-and-Navigation.md b/Wiki/Pages-and-Navigation.md new file mode 100644 index 0000000..0f2f3d9 --- /dev/null +++ b/Wiki/Pages-and-Navigation.md @@ -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) diff --git a/Wiki/Server-API.md b/Wiki/Server-API.md new file mode 100644 index 0000000..cb0c517 --- /dev/null +++ b/Wiki/Server-API.md @@ -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) diff --git a/Wiki/State-Management.md b/Wiki/State-Management.md new file mode 100644 index 0000000..a86e94d --- /dev/null +++ b/Wiki/State-Management.md @@ -0,0 +1,111 @@ +# 状态管理 + +## AppProvider + +`AppProvider`(`lib/providers/app_provider.dart`)是应用唯一的 ChangeNotifier,通过 `MultiProvider` 在根节点注入,所有页面通过 `context.watch()` 或 `context.read()` 访问。 + +## 管理的状态 + +### 数据列表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `_movies` | `List` | 影视数据 | +| `_books` | `List` | 书籍数据 | +| `_notes` | `List` | 笔记数据 | + +### 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) diff --git a/Wiki/Sync-System.md b/Wiki/Sync-System.md new file mode 100644 index 0000000..7cd41c8 --- /dev/null +++ b/Wiki/Sync-System.md @@ -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) diff --git a/Wiki/Theme-and-UI.md b/Wiki/Theme-and-UI.md new file mode 100644 index 0000000..95f2247 --- /dev/null +++ b/Wiki/Theme-and-UI.md @@ -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) diff --git a/Wiki/User-Preferences.md b/Wiki/User-Preferences.md new file mode 100644 index 0000000..08efdac --- /dev/null +++ b/Wiki/User-Preferences.md @@ -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) diff --git a/lib/pages/app_icon_picker_page.dart b/lib/pages/app_icon_picker_page.dart index bd1e749..36f1ecc 100644 --- a/lib/pages/app_icon_picker_page.dart +++ b/lib/pages/app_icon_picker_page.dart @@ -83,7 +83,8 @@ class _AppIconPickerPageState extends State { 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 { 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 { 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)), ], ), ), diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart index 392d570..ebe5653 100644 --- a/lib/pages/book/book_detail_page.dart +++ b/lib/pages/book/book_detail_page.dart @@ -686,8 +686,9 @@ class _BookDetailPageState extends State { } void _navigateToEdit(BuildContext context) { + final provider = context.read(); Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) { - context.read().loadBooks(); + provider.loadBooks(); }); } diff --git a/lib/pages/movies/movie_detail_page.dart b/lib/pages/movies/movie_detail_page.dart index f6ac6a4..efe7860 100644 --- a/lib/pages/movies/movie_detail_page.dart +++ b/lib/pages/movies/movie_detail_page.dart @@ -692,8 +692,9 @@ class _MovieDetailPageState extends State { } void _navigateToEdit(BuildContext context) { + final provider = context.read(); Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) { - context.read().loadMovies(); + provider.loadMovies(); }); } diff --git a/lib/pages/note/note_detail_page.dart b/lib/pages/note/note_detail_page.dart index 7d616be..26021cf 100644 --- a/lib/pages/note/note_detail_page.dart +++ b/lib/pages/note/note_detail_page.dart @@ -291,8 +291,9 @@ class _NoteDetailPageState extends State { (n) => n.id == widget.note.id, orElse: () => widget.note, ); + final provider = context.read(); Navigator.pushNamed(context, '/note-form', arguments: currentNote).then((_) async { - await context.read().loadNotes(); + await provider.loadNotes(); }); } diff --git a/lib/widgets/note_list_item.dart b/lib/widgets/note_list_item.dart index 13ecffc..d08f59d 100644 --- a/lib/widgets/note_list_item.dart +++ b/lib/widgets/note_list_item.dart @@ -30,8 +30,9 @@ class _NoteListItemContent extends StatelessWidget { final colors = Theme.of(context).colorScheme; return InkWell( onTap: () { + final provider = context.read(); Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) async { - await context.read().loadNotes(); + await provider.loadNotes(); }); }, onLongPress: () => _showDeleteDialog(context),