基础功能

This commit is contained in:
DelLevin-Home
2026-03-05 10:06:32 +08:00
commit adad6372a3
55 changed files with 6036 additions and 0 deletions

63
.gitignore vendored Normal file
View File

@@ -0,0 +1,63 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VS Code
.vscode/
# Flutter/Dart/Pub
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Android
android/.gradle/
android/captures/
android/gradlew
android/gradlew.bat
android/local.properties
android/**/GeneratedPluginRegistrant.java
android/app/src/main/jniSrc/
# iOS/XCode
ios/Flutter/*.framework
ios/Flutter/Flutter.framework
ios/Flutter/Flutter.podspec
ios/Flutter/Generated.xcconfig
ios/Flutter/ephemeral
ios/Flutter/app.flx
ios/Flutter/app.zip
ios/Flutter/flutter_assets/
ios/Flutter/flutter_export_environment.sh
ios/ServiceDefinitions.json
ios/Runner/GeneratedPluginRegistrant.*
# Exceptions
!android/gradle/wrapper/gradle-wrapper.jar
!ios/Flutter/Debug.xcconfig
!ios/Flutter/Release.xcconfig
# Coverage
coverage/
# Temporary files
*.tmp
*.temp

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "48c32af0345e9ad5747f78ddce828c7f795f7159"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
base_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
- platform: android
create_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
base_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

251
QUICKSTART.md Normal file
View File

@@ -0,0 +1,251 @@
# MookNote 快速入门指南
## 第一步:安装 Flutter 开发环境
### Windows 系统详细步骤
#### 1. 下载 Flutter SDK
访问https://docs.flutter.dev/get-started/install/windows
- 点击 "Download Flutter SDK"
- 下载完成后解压到 `C:\src\flutter`
#### 2. 添加环境变量
```
1. 右键"此电脑" → "属性" → "高级系统设置"
2. 点击"环境变量"
3. 在"系统变量"中找到 "Path",点击"编辑"
4. 点击"新建"添加C:\src\flutter\bin
5. 确定保存
```
#### 3. 验证安装
打开命令提示符cmd或 PowerShell
```bash
flutter --version
flutter doctor
```
#### 4. 安装 Android Studio
- 下载地址https://developer.android.com/studio
- 安装完成后打开 Android Studio
- 配置 Android SDK
- File → Settings → Appearance & Behavior → System Settings → Android SDK
- 选择 "SDK Platforms" 标签
- 勾选 "Android 13.0 (API 33)" 或更高版本
- 选择 "SDK Tools" 标签
- 勾选 "Android SDK Build-Tools" 和 "Android Emulator"
- 点击 Apply 开始安装
#### 5. 接受 Android 许可证
```bash
flutter doctor --android-licenses
```
按提示输入 `y` 接受所有许可证
#### 6. 再次运行 flutter doctor
```bash
flutter doctor
```
确保看到类似以下输出:
```
[✓] Flutter (Channel stable, 3.x.x, on Microsoft Windows)
[✓] Android toolchain - develop for Android devices
[✓] Android Studio
[✓] Connected device
```
## 第二步:运行 MookNote 项目
### 1. 进入项目目录
```bash
cd d:\UserData\Desktop\my_proj\mooknote
```
### 2. 安装依赖包
```bash
flutter pub get
```
### 3. 准备设备
**选项 A使用真机**
```
1. 手机开启"开发者选项"
- 设置 → 关于手机 → 连续点击"版本号"7 次
2. 开启"USB 调试"
- 设置 → 开发者选项 → USB 调试
3. 用 USB 连接电脑
4. 手机上允许 USB 调试授权
```
**选项 B使用模拟器**
```
1. Android Studio → Tools → Device Manager
2. Click "Create device"
3. 选择设备型号(如 Pixel 6
4. 下载并选择系统镜像(推荐 API 33
5. 完成创建后点击启动按钮
```
### 4. 运行应用
```bash
flutter run
```
或者在 Android Studio 中:
- File → Open → 选择 mooknote 文件夹
- 点击顶部工具栏的运行按钮(绿色三角形)
## 第三步:理解代码结构
### 核心文件说明
#### 1. 入口文件 - `lib/main.dart`
```dart
void main() {
runApp(const MyApp());
}
```
应用的起点,配置主题和路由
#### 2. 主页面 - `lib/pages/home_page.dart`
包含:
- 顶部 AppBar菜单按钮 + 标题)
- 三个标签页切换(观影/阅读/笔记)
- 主体内容区域
#### 3. 状态管理 - `lib/providers/app_provider.dart`
管理全局状态:
- 当前选中的标签页
- 当前选中的状态筛选
- 侧边菜单开关
#### 4. 数据模型 - `lib/models/data_models.dart`
定义数据结构:
- `Movie` - 电影数据
- `Book` - 书籍数据
- `Note` - 笔记数据
### Widget 组件层次
```
MyApp
└── HomePage (主页面)
├── CustomDrawer (侧边菜单)
│ └── 热力图组件
├── AppBar (顶部栏)
├── TabBar (标签栏)
│ ├── 观影
│ ├── 阅读
│ └── 笔记
├── TabContent (标签内容)
│ ├── MovieTabPage
│ │ ├── MovieStatusBar (状态栏)
│ │ └── MovieListItem (列表项)
│ ├── BookTabPage
│ │ ├── BookStatusBar (状态栏)
│ │ └── BookListItem (列表项)
│ └── NoteTabPage
│ └── NoteListItem (列表项)
└── CustomBottomNavBar (底部导航)
```
## 第四步:修改和定制
### 修改应用名称
编辑 `pubspec.yaml`:
```yaml
name: mooknote # 包名
```
编辑 `android/app/src/main/AndroidManifest.xml`:
```xml
android:label="MookNote" <!-- 显示名称 -->
```
### 修改主题颜色
编辑 `lib/utils/app_theme.dart`:
```dart
static const Color primaryColor = Color(0xFF6200EE); // 主色调
```
### 添加示例数据
编辑对应的 page 文件,例如 `lib/pages/movie_tab_page.dart`:
```dart
List<Movie> _getSampleMovies(int statusIndex) {
// 在这里添加你的电影数据
}
```
## 第五步:构建 APK
### 开发版本
```bash
flutter build apk --debug
```
### 发布版本
```bash
flutter build apk --release
```
生成的 APK 位置:
```
build/app/outputs/flutter-apk/app-release.apk
```
## 常见问题解决
### Q1: flutter pub get 失败
```bash
# 清除缓存
flutter clean
flutter pub cache repair
flutter pub get
```
### Q2: 无法识别设备
```bash
# 查看设备列表
flutter devices
# 如果没有设备,检查:
# 1. USB 线是否连接好
# 2. 手机 USB 调试是否开启
# 3. 是否安装了手机驱动
```
### Q3: 热重载不工作
`R` 进行热重载,按 `r` 完全重启
如果都不行,停止应用重新运行
### Q4: 中文乱码
确保所有文件使用 UTF-8 编码
## 下一步学习建议
### 1. 学习 Dart 基础语法1-2 天)
- 变量和类型
- 函数
- 类和对象
- 异步编程async/await
### 2. 学习 Flutter 基础3-5 天)
- Widget 概念
- StatelessWidget vs StatefulWidget
- 布局组件Row, Column, Container 等)
- 列表ListView
### 3. 实践项目功能
- 实现添加功能表单
- 集成 SQLite 数据库
- 实现搜索功能
- 完善详情页
### 4. 参考资源
- [Dart 语言中文文档](https://dart.cn/)
- [Flutter 实战电子书](https://book.flutterchina.club/)
- [B 站 Flutter 教程](https://search.bilibili.com/all?keyword=flutter 教程)
---
祝你开发顺利!有问题随时询问。

219
README.md Normal file
View File

@@ -0,0 +1,219 @@
# MookNote - Flutter 观影阅读笔记应用
## 项目简介
MookNote 是一款用于记录观影、阅读和笔记的 Android 应用,使用 Flutter 框架开发。
## 功能特性
### 主界面
- **顶部导航栏**: 左侧菜单按钮 + 标题 + 搜索按钮
- **三个标签页**: 观影、阅读、笔记
- **底部导航栏**: 主页、新增、我的
### 观影模块
- 三个状态筛选:已看、想看、在看
- 显示影片标题、年份、评分、观看日期、笔记
- 支持添加和管理观影记录
### 阅读模块
- 三个状态筛选:读完、在读、准备读
- 显示书籍标题、作者、评分、阅读日期、笔记
- 支持添加和管理书籍记录
### 笔记模块
- 显示所有笔记列表
- 支持标签分类
- 显示笔记内容和更新时间
### 侧边菜单
- GitHub 风格热力图(年度记录可视化)
- 统计功能入口
- 回收站入口
- 设置入口
## 环境要求
### 必需软件
1. **Flutter SDK** (建议 3.5.0+)
- 下载地址https://docs.flutter.dev/get-started/install
2. **Android Studio**
- 下载地址https://developer.android.com/studio
3. **Android SDK** (通过 Android Studio 安装)
### 环境配置步骤
#### Windows 系统
1. **下载并安装 Flutter SDK**
```
- 下载 Flutter SDK zip 文件
- 解压到 C:\src\flutter或其他非系统目录
- 将 C:\src\flutter\bin 添加到系统环境变量 Path
```
2. **验证 Flutter 安装**
```bash
flutter --version
flutter doctor
```
3. **安装 Android Studio**
```
- 下载并安装 Android Studio
- 打开 Android Studio → Settings → Appearance & Behavior → System Settings → Android SDK
- 安装 Android SDK Platform建议 API 33+
- 安装 Android SDK Build-Tools
- 安装 Android Emulator可选用于模拟器测试
```
4. **接受 Android 许可证**
```bash
flutter doctor --android-licenses
```
```bash
$env:HTTP_PROXY="http://127.0.0.1:10808"
$env:HTTPS_PROXY="http://127.0.0.1:10808"
flutter create --platforms android .
```
## 运行项目
### 1. 克隆/复制项目后,进入项目目录
```bash
cd mooknote
```
### 2. 安装依赖
```bash
flutter pub get
```
### 3. 连接设备或启动模拟器
- 连接 Android 真机(需开启 USB 调试)
- 或启动 Android 模拟器
### 4. 运行应用
```bash
flutter run
```
或使用 Android Studio:
- 打开项目
- 点击运行按钮 (Run)
## 项目结构
```
mooknote/
├── android/ # Android 平台配置
│ └── app/
│ └── src/main/
│ ├── AndroidManifest.xml
│ └── ...
├── lib/ # Dart 源代码
│ ├── main.dart # 应用入口
│ ├── pages/ # 页面组件
│ │ ├── home_page.dart # 主页面
│ │ ├── movie_tab_page.dart # 观影标签页
│ │ ├── book_tab_page.dart # 阅读标签页
│ │ └── note_tab_page.dart # 笔记标签页
│ ├── widgets/ # 可复用组件
│ │ ├── custom_drawer.dart # 侧边菜单
│ │ ├── bottom_nav_bar.dart # 底部导航
│ │ ├── movie_status_bar.dart # 观影状态栏
│ │ ├── movie_list_item.dart # 观影列表项
│ │ ├── book_status_bar.dart # 书籍状态栏
│ │ ├── book_list_item.dart # 书籍列表项
│ │ └── note_list_item.dart # 笔记列表项
│ ├── models/ # 数据模型
│ │ └── data_models.dart # Movie, Book, Note 模型
│ ├── providers/ # 状态管理
│ │ └── app_provider.dart # 全局状态管理
│ └── utils/ # 工具类
│ └── app_theme.dart # 主题配置
├── assets/ # 资源文件
│ └── images/ # 图片资源
├── fonts/ # 字体文件
├── pubspec.yaml # 项目配置文件
└── README.md # 本文件
```
## 技术栈
- **Flutter**: 跨平台移动应用框架
- **Dart**: 编程语言
- **Provider**: 状态管理
- **Material Design 3**: UI 设计规范
## 后续开发计划
### 数据存储
- [x] 集成 SQLite 数据库sqflite 包)
- [x] 实现本地数据持久化
- [ ] 数据备份与恢复
### 功能增强
- [x] 添加/编辑表单页面
- [ ] 详情页完整实现
- [ ] 搜索功能
- [ ] 统计图表页面
- [ ] 回收站功能
- [ ] 设置页面
### 用户体验
- [ ] 加载动画
- [ ] 下拉刷新
- [ ] 无限滚动加载
- [ ] 深色模式优化
### 高级功能
- [ ] 导出功能JSON/CSV
- [ ] 云同步
- [ ] 桌面小组件
## 常见问题
### Q: flutter doctor 显示 Android license status unknown
A: 运行 `flutter doctor --android-licenses` 接受许可证
### Q: 模拟器无法启动
A: 确保在 BIOS 中启用了虚拟化技术VT-x/AMD-V
### Q: 热重载不生效
A: 某些代码更改需要完全重启应用(如 main() 函数修改)
## 开发建议
### 对于有 Vue 经验的你
Flutter 的很多概念与 Vue 相似:
- **Widget** ≈ Vue 组件
- **StatefulWidget** ≈ 带数据的 Vue 组件
- **setState()** ≈ this.$forceUpdate() + 数据更新
- **Provider** ≈ Vuex/Pinia
- **pubspec.yaml** ≈ package.json
### 学习资源
- [Flutter 官方文档](https://docs.flutter.dev/)
- [Dart 语言指南](https://dart.dev/guides)
- [Flutter 中文社区](https://flutterchina.club/)
## 许可证
MIT License
---
**作者**: 你的昵称
**创建时间**: 2024-03-04

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.mooknote"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.mooknote"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="MookNote"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.example.mooknote
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=10808
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=10808

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

41
lib/main.dart Normal file
View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'pages/home_page.dart';
import 'utils/app_theme.dart';
import 'utils/app_router.dart';
import 'providers/app_provider.dart';
void main() async {
// 确保 Flutter 绑定初始化完成
WidgetsFlutterBinding.ensureInitialized();
// 初始化数据库
final appProvider = AppProvider();
await appProvider.initDatabase();
runApp(MyApp(appProvider: appProvider));
}
class MyApp extends StatelessWidget {
final AppProvider appProvider;
const MyApp({super.key, required this.appProvider});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: appProvider),
],
child: MaterialApp(
title: 'MookNote',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const HomePage(),
onGenerateRoute: AppRouter.generateRoute,
),
);
}
}

148
lib/models/data_models.dart Normal file
View File

@@ -0,0 +1,148 @@
/// 影视条目模型
class Movie {
final String id;
final String title;
final String? poster;
final double? rating;
final int? year;
final String status; // 'watched', 'want_to_watch', 'watching'
final DateTime? watchDate;
final String? note;
Movie({
required this.id,
required this.title,
this.poster,
this.rating,
this.year,
required this.status,
this.watchDate,
this.note,
});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
id: json['id'] ?? '',
title: json['title'] ?? '',
poster: json['poster'],
rating: json['rating']?.toDouble(),
year: json['year'],
status: json['status'] ?? 'want_to_watch',
watchDate: json['watch_date'] != null
? DateTime.parse(json['watch_date'])
: null,
note: json['note'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'poster': poster,
'rating': rating,
'year': year,
'status': status,
'watch_date': watchDate?.toIso8601String(),
'note': note,
};
}
}
/// 书籍条目模型
class Book {
final String id;
final String title;
final String? author;
final String? cover;
final double? rating;
final String status; // 'read', 'reading', 'want_to_read'
final DateTime? readDate;
final String? note;
Book({
required this.id,
required this.title,
this.author,
this.cover,
this.rating,
required this.status,
this.readDate,
this.note,
});
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
id: json['id'] ?? '',
title: json['title'] ?? '',
author: json['author'],
cover: json['cover'],
rating: json['rating']?.toDouble(),
status: json['status'] ?? 'want_to_read',
readDate: json['read_date'] != null
? DateTime.parse(json['read_date'])
: null,
note: json['note'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'author': author,
'cover': cover,
'rating': rating,
'status': status,
'read_date': readDate?.toIso8601String(),
'note': note,
};
}
}
/// 笔记模型
class Note {
final String id;
final String title;
final String content;
final List<String> tags;
final DateTime createdAt;
final DateTime updatedAt;
Note({
required this.id,
required this.title,
required this.content,
this.tags = const [],
required this.createdAt,
required this.updatedAt,
});
factory Note.fromJson(Map<String, dynamic> json) {
return Note(
id: json['id'] ?? '',
title: json['title'] ?? '',
content: json['content'] ?? '',
tags: json['tags'] != null
? List<String>.from(json['tags'])
: [],
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'])
: DateTime.now(),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'])
: DateTime.now(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'tags': tags,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
}

View File

@@ -0,0 +1,78 @@
/// 数据模型扩展 - 添加 copyWith 方法以便更新数据
library;
import 'data_models.dart';
/// Movie 扩展 - 添加 copyWith 方法
extension MovieExtension on Movie {
/// 创建副本并允许修改部分属性
Movie copyWith({
String? id,
String? title,
String? poster,
double? rating,
int? year,
String? status,
DateTime? watchDate,
String? note,
}) {
return Movie(
id: id ?? this.id,
title: title ?? this.title,
poster: poster ?? this.poster,
rating: rating ?? this.rating,
year: year ?? this.year,
status: status ?? this.status,
watchDate: watchDate ?? this.watchDate,
note: note ?? this.note,
);
}
}
/// Book 扩展 - 添加 copyWith 方法
extension BookExtension on Book {
/// 创建副本并允许修改部分属性
Book copyWith({
String? id,
String? title,
String? author,
String? cover,
double? rating,
String? status,
DateTime? readDate,
String? note,
}) {
return Book(
id: id ?? this.id,
title: title ?? this.title,
author: author ?? this.author,
cover: cover ?? this.cover,
rating: rating ?? this.rating,
status: status ?? this.status,
readDate: readDate ?? this.readDate,
note: note ?? this.note,
);
}
}
/// Note 扩展 - 添加 copyWith 方法
extension NoteExtension on Note {
/// 创建副本并允许修改部分属性
Note copyWith({
String? id,
String? title,
String? content,
List<String>? tags,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Note(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
tags: tags ?? this.tags,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}

View File

@@ -0,0 +1,293 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 书籍详情页
class BookDetailPage extends StatefulWidget {
final Book book;
const BookDetailPage({super.key, required this.book});
@override
State<BookDetailPage> createState() => _BookDetailPageState();
}
class _BookDetailPageState extends State<BookDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.book.title),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _navigateToEdit(context),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _showDeleteDialog(context),
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面区域
_buildCoverSection(context),
// 基本信息
_buildInfoSection(context),
// 笔记区域
if (widget.book.note != null && widget.book.note!.isNotEmpty)
_buildNoteSection(context),
],
),
),
);
}
/// 构建封面区域
Widget _buildCoverSection(BuildContext context) {
return Container(
width: double.infinity,
height: 250,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.surface,
],
),
),
child: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.menu_book,
size: 80,
color: Theme.of(context).colorScheme.primary.withOpacity(0.6),
),
const SizedBox(height: 16),
Text(
widget.book.title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
Positioned(
top: 16,
right: 16,
child: _buildStatusTag(context),
),
],
),
);
}
/// 构建状态标签
Widget _buildStatusTag(BuildContext context) {
Color statusColor;
String statusText;
switch (widget.book.status) {
case 'read':
statusColor = AppTheme.readColor;
statusText = '读完';
break;
case 'reading':
statusColor = AppTheme.readingColor;
statusText = '在读';
break;
case 'want_to_read':
statusColor = AppTheme.wantToReadColor;
statusText = '准备读';
break;
default:
statusColor = Colors.grey;
statusText = '未知';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.9),
borderRadius: BorderRadius.circular(20),
),
child: Text(
statusText,
style: const TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
}
/// 构建信息区域
Widget _buildInfoSection(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 作者
if (widget.book.author != null && widget.book.author!.isNotEmpty) ...[
_buildInfoItem(
context,
icon: Icons.person,
label: widget.book.author!,
),
const SizedBox(height: 12),
],
// 评分
if (widget.book.rating != null) ...[
_buildInfoItem(
context,
icon: Icons.star,
label: widget.book.rating.toString(),
iconColor: Colors.amber[700],
textColor: Colors.amber[700],
),
const SizedBox(height: 12),
],
// 阅读日期
if (widget.book.readDate != null)
_buildInfoItem(
context,
icon: Icons.event,
label: '阅读日期:${_formatDate(widget.book.readDate!)}',
),
],
),
);
}
/// 构建信息项
Widget _buildInfoItem(
BuildContext context, {
required IconData icon,
required String label,
Color? iconColor,
Color? textColor,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: iconColor),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
/// 构建笔记区域
Widget _buildNoteSection(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.edit_note,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'笔记',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
Text(
widget.book.note!,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
context.read<AppProvider>().loadBooks();
});
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${widget.book.title}"吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeBook(widget.book.id);
if (!mounted) return;
Navigator.pop(context);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,287 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 添加/编辑书籍记录页面
class BookFormPage extends StatefulWidget {
final Book? book; // 如果为 null则是添加模式否则是编辑模式
const BookFormPage({super.key, this.book});
@override
State<BookFormPage> createState() => _BookFormPageState();
}
class _BookFormPageState extends State<BookFormPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _titleController;
late TextEditingController _authorController;
late TextEditingController _ratingController;
late TextEditingController _noteController;
late String _status;
DateTime? _readDate;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.book?.title ?? '');
_authorController = TextEditingController(text: widget.book?.author ?? '');
_ratingController = TextEditingController(text: widget.book?.rating?.toString() ?? '');
_noteController = TextEditingController(text: widget.book?.note ?? '');
_status = widget.book?.status ?? 'want_to_read';
_readDate = widget.book?.readDate;
}
@override
void dispose() {
_titleController.dispose();
_authorController.dispose();
_ratingController.dispose();
_noteController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isEdit = widget.book != null;
return Scaffold(
appBar: AppBar(
title: Text(isEdit ? '编辑书籍' : '添加书籍'),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveBook,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '书名 *',
hintText: '请输入书名',
prefixIcon: Icon(Icons.menu_book),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入书名';
}
return null;
},
),
const SizedBox(height: 16),
// 作者
TextFormField(
controller: _authorController,
decoration: const InputDecoration(
labelText: '作者',
hintText: '请输入作者',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 年份和评分
Row(
children: [
Expanded(
child: TextFormField(
controller: _ratingController,
decoration: const InputDecoration(
labelText: '评分',
hintText: '0-10',
prefixIcon: Icon(Icons.star),
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (value) {
if (value != null && value.isNotEmpty) {
final rating = double.tryParse(value);
if (rating == null || rating < 0 || rating > 10) {
return '评分必须在 0-10 之间';
}
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// 状态选择
DropdownButtonFormField<String>(
value: _status,
decoration: const InputDecoration(
labelText: '状态',
prefixIcon: Icon(Icons.check_circle_outline),
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(value: 'read', child: Text('读完')),
DropdownMenuItem(value: 'reading', child: Text('在读')),
DropdownMenuItem(value: 'want_to_read', child: Text('准备读')),
],
onChanged: (value) {
setState(() {
_status = value!;
});
},
),
const SizedBox(height: 16),
// 阅读日期选择
InkWell(
onTap: _selectReadDate,
child: InputDecorator(
decoration: const InputDecoration(
labelText: '阅读日期',
prefixIcon: Icon(Icons.event),
border: OutlineInputBorder(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_readDate != null
? '${_readDate!.year}-${_readDate!.month.toString().padLeft(2, '0')}-${_readDate!.day.toString().padLeft(2, '0')}'
: '选择日期',
style: TextStyle(
color: _readDate != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (_readDate != null)
IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
setState(() {
_readDate = null;
});
},
),
],
),
),
),
const SizedBox(height: 16),
// 笔记
TextFormField(
controller: _noteController,
decoration: const InputDecoration(
labelText: '笔记',
hintText: '写下你的读后感...',
prefixIcon: Icon(Icons.edit_note),
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
maxLines: 5,
),
const SizedBox(height: 32),
// 保存按钮
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
onPressed: _saveBook,
icon: const Icon(Icons.save),
label: Text(isEdit ? '保存修改' : '添加记录'),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
),
),
);
}
/// 选择阅读日期
Future<void> _selectReadDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _readDate ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_readDate = picked;
});
}
}
/// 保存书籍记录
Future<void> _saveBook() async {
if (!_formKey.currentState!.validate()) {
return;
}
final rating = _ratingController.text.isNotEmpty ? double.tryParse(_ratingController.text) : null;
if (widget.book == null) {
// 添加新模式
final newBook = Book(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: _titleController.text.trim(),
author: _authorController.text.trim(),
rating: rating,
status: _status,
readDate: _readDate,
note: _noteController.text.trim(),
);
await context.read<AppProvider>().addBook(newBook);
} else {
// 编辑现有模式
final updatedBook = Book(
id: widget.book!.id,
title: _titleController.text.trim(),
author: _authorController.text.trim(),
rating: rating,
status: _status,
readDate: _readDate,
note: _noteController.text.trim(),
);
await context.read<AppProvider>().updateBook(updatedBook);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.book == null ? '添加成功' : '更新成功'),
behavior: SnackBarBehavior.floating,
),
);
Navigator.pop(context);
}
}

View File

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../widgets/book_status_bar.dart';
import '../widgets/book_list_item.dart';
/// 阅读标签页
class BookTabPage extends StatelessWidget {
const BookTabPage({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 状态选择栏(读完、在读、准备读)
const BookStatusBar(),
// 书籍列表
Expanded(
child: _buildBookList(context),
),
],
);
}
/// 构建书籍列表
Widget _buildBookList(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
// 根据状态筛选书籍
final statusMap = {
0: 'read',
1: 'reading',
2: 'want_to_read',
};
final currentStatus = statusMap[provider.bookStatusIndex]!;
final books = provider.getBooksByStatus(currentStatus);
if (books.isEmpty) {
return _buildEmptyState(context, provider.bookStatusIndex);
}
return RefreshIndicator(
onRefresh: () async => await provider.loadBooks(),
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: books.length,
itemBuilder: (context, index) {
return BookListItem(book: books[index]);
},
),
);
},
);
}
/// 构建空状态提示
Widget _buildEmptyState(BuildContext context, int statusIndex) {
final statusText = ['读完', '在读', '准备读'][statusIndex];
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.menu_book_outlined,
size: 80,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'暂无$statusText的书籍',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('添加记录'),
onPressed: () {
Navigator.pushNamed(context, '/book-form');
},
),
],
),
);
}
/// 获取示例书籍数据
List<Book> _getSampleBooks(int statusIndex) {
final statusMap = ['read', 'reading', 'want_to_read'];
final currentStatus = statusMap[statusIndex];
// 示例数据(实际应从数据库获取)
final allBooks = [
Book(
id: '1',
title: '活着',
author: '余华',
rating: 9.2,
status: 'read',
readDate: DateTime(2024, 1, 20),
note: '非常感人的故事,让人思考生命的意义',
),
Book(
id: '2',
title: '百年孤独',
author: '加西亚·马尔克斯',
rating: 9.3,
status: 'read',
readDate: DateTime(2024, 2, 15),
),
Book(
id: '3',
title: '人类简史',
author: '尤瓦尔·赫拉利',
rating: 9.0,
status: 'reading',
),
Book(
id: '4',
title: '三体',
author: '刘慈欣',
rating: 9.5,
status: 'want_to_read',
),
Book(
id: '5',
title: '追风筝的人',
author: '卡勒德·胡赛尼',
rating: 8.9,
status: 'read',
readDate: DateTime(2024, 3, 5),
note: '关于救赎与成长的故事',
),
];
return allBooks.where((b) => b.status == currentStatus).toList();
}
}

247
lib/pages/home_page.dart Normal file
View File

@@ -0,0 +1,247 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../widgets/custom_drawer.dart';
import '../widgets/bottom_nav_bar.dart';
import 'movie_tab_page.dart';
import 'book_tab_page.dart';
import 'note_tab_page.dart';
/// 主页 - 包含顶部菜单、三个标签页、底部导航
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
// 左侧弹出菜单
drawer: const CustomDrawer(),
// 主体内容
body: Column(
children: [
// 顶部 AppBar
_buildAppBar(),
// 三个标签页的标题栏
_buildTabBar(),
// 标签页内容
Expanded(
child: _buildTabContent(),
),
],
),
// 底部导航栏
bottomNavigationBar: const CustomBottomNavBar(),
);
}
/// 构建顶部 AppBar
Widget _buildAppBar() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return AppBar(
title: Text(_getAppBarTitle(provider)),
actions: [
// 添加按钮
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context, provider),
),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// TODO: 搜索功能
},
),
],
);
},
);
}
/// 获取 AppBar 标题
String _getAppBarTitle(AppProvider provider) {
switch (provider.mainTabIndex) {
case 0:
return '观影';
case 1:
return '阅读';
case 2:
return '笔记';
default:
return 'MookNote';
}
}
/// 构建标签栏
Widget _buildTabBar() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// 观影
_buildTabItem(
context,
'观影',
Icons.movie,
0,
provider.mainTabIndex,
() => provider.setMainTabIndex(0),
),
// 阅读
_buildTabItem(
context,
'阅读',
Icons.menu_book,
1,
provider.mainTabIndex,
() => provider.setMainTabIndex(1),
),
// 笔记
_buildTabItem(
context,
'笔记',
Icons.note,
2,
provider.mainTabIndex,
() => provider.setMainTabIndex(2),
),
],
),
);
},
);
}
/// 构建单个标签项
Widget _buildTabItem(
BuildContext context,
String label,
IconData icon,
int index,
int currentIndex,
VoidCallback onTap,
) {
final isSelected = index == currentIndex;
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
),
),
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 8),
width: 32,
height: 3,
decoration: BoxDecoration(
color: colorScheme.primary,
borderRadius: BorderRadius.circular(2),
),
),
],
),
),
),
);
}
/// 构建标签页内容
Widget _buildTabContent() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
switch (provider.mainTabIndex) {
case 0:
return const MovieTabPage();
case 1:
return const BookTabPage();
case 2:
return const NoteTabPage();
default:
return const MovieTabPage();
}
},
);
}
/// 显示添加对话框
void _showAddDialog(BuildContext context, AppProvider provider) {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.movie, color: Colors.green),
title: const Text('添加观影'),
subtitle: const Text('记录你看过的电影'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/movie-form');
},
),
ListTile(
leading: const Icon(Icons.menu_book, color: Colors.orange),
title: const Text('添加阅读'),
subtitle: const Text('记录你读过的书'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/book-form');
},
),
ListTile(
leading: const Icon(Icons.note, color: Colors.blue),
title: const Text('添加笔记'),
subtitle: const Text('记录你的想法和笔记'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/note-form');
},
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,284 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 影视详情页
class MovieDetailPage extends StatefulWidget {
final Movie movie;
const MovieDetailPage({super.key, required this.movie});
@override
State<MovieDetailPage> createState() => _MovieDetailPageState();
}
class _MovieDetailPageState extends State<MovieDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.movie.title),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _navigateToEdit(context),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _showDeleteDialog(context),
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 海报区域
_buildPosterSection(context),
// 基本信息
_buildInfoSection(context),
// 笔记区域
if (widget.movie.note != null && widget.movie.note!.isNotEmpty)
_buildNoteSection(context),
],
),
),
);
}
/// 构建海报区域
Widget _buildPosterSection(BuildContext context) {
return Container(
width: double.infinity,
height: 300,
decoration: BoxDecoration(
color: Colors.grey[300],
),
child: Stack(
children: [
Center(
child: Icon(
Icons.movie,
size: 80,
color: Colors.grey[500],
),
),
Positioned(
top: 16,
right: 16,
child: _buildStatusTag(context),
),
],
),
);
}
/// 构建状态标签
Widget _buildStatusTag(BuildContext context) {
Color statusColor;
String statusText;
switch (widget.movie.status) {
case 'watched':
statusColor = AppTheme.watchedColor;
statusText = '已看';
break;
case 'want_to_watch':
statusColor = AppTheme.wantToWatchColor;
statusText = '想看';
break;
case 'watching':
statusColor = AppTheme.watchingColor;
statusText = '在看';
break;
default:
statusColor = Colors.grey;
statusText = '未知';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.9),
borderRadius: BorderRadius.circular(20),
),
child: Text(
statusText,
style: const TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
}
/// 构建信息区域
Widget _buildInfoSection(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
widget.movie.title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 年份和评分
Row(
children: [
if (widget.movie.year != null) ...[
_buildInfoItem(
context,
icon: Icons.calendar_today,
label: '${widget.movie.year}',
),
const SizedBox(width: 16),
],
if (widget.movie.rating != null) ...[
_buildInfoItem(
context,
icon: Icons.star,
label: widget.movie.rating.toString(),
iconColor: Colors.amber[700],
textColor: Colors.amber[700],
),
],
],
),
const SizedBox(height: 16),
// 观看日期
if (widget.movie.watchDate != null)
_buildInfoItem(
context,
icon: Icons.event,
label: '观看日期:${_formatDate(widget.movie.watchDate!)}',
),
],
),
);
}
/// 构建信息项
Widget _buildInfoItem(
BuildContext context, {
required IconData icon,
required String label,
Color? iconColor,
Color? textColor,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: iconColor),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
/// 构建笔记区域
Widget _buildNoteSection(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.edit_note,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'笔记',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
Text(
widget.movie.note!,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
// 返回后刷新数据
context.read<AppProvider>().loadMovies();
});
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${widget.movie.title}"吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMovie(widget.movie.id);
if (!context.mounted) return;
Navigator.pop(context); // 关闭对话框
Navigator.pop(context); // 返回上一页
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,290 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 添加/编辑影视记录页面
class MovieFormPage extends StatefulWidget {
final Movie? movie; // 如果为 null则是添加模式否则是编辑模式
const MovieFormPage({super.key, this.movie});
@override
State<MovieFormPage> createState() => _MovieFormPageState();
}
class _MovieFormPageState extends State<MovieFormPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _titleController;
late TextEditingController _yearController;
late TextEditingController _ratingController;
late TextEditingController _noteController;
late String _status;
DateTime? _watchDate;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.movie?.title ?? '');
_yearController = TextEditingController(text: widget.movie?.year?.toString() ?? '');
_ratingController = TextEditingController(text: widget.movie?.rating?.toString() ?? '');
_noteController = TextEditingController(text: widget.movie?.note ?? '');
_status = widget.movie?.status ?? 'want_to_watch';
_watchDate = widget.movie?.watchDate;
}
@override
void dispose() {
_titleController.dispose();
_yearController.dispose();
_ratingController.dispose();
_noteController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isEdit = widget.movie != null;
return Scaffold(
appBar: AppBar(
title: Text(isEdit ? '编辑影片' : '添加影片'),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveMovie,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '影片名称 *',
hintText: '请输入影片名称',
prefixIcon: Icon(Icons.movie),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入影片名称';
}
return null;
},
),
const SizedBox(height: 16),
// 年份和评分
Row(
children: [
Expanded(
child: TextFormField(
controller: _yearController,
decoration: const InputDecoration(
labelText: '年份',
hintText: '例如2024',
prefixIcon: Icon(Icons.calendar_today),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _ratingController,
decoration: const InputDecoration(
labelText: '评分',
hintText: '0-10',
prefixIcon: Icon(Icons.star),
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (value) {
if (value != null && value.isNotEmpty) {
final rating = double.tryParse(value);
if (rating == null || rating < 0 || rating > 10) {
return '评分必须在 0-10 之间';
}
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// 状态选择
DropdownButtonFormField<String>(
value: _status,
decoration: const InputDecoration(
labelText: '状态',
prefixIcon: Icon(Icons.check_circle_outline),
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(value: 'watched', child: Text('已看')),
DropdownMenuItem(value: 'want_to_watch', child: Text('想看')),
DropdownMenuItem(value: 'watching', child: Text('在看')),
],
onChanged: (value) {
setState(() {
_status = value!;
});
},
),
const SizedBox(height: 16),
// 观看日期选择
InkWell(
onTap: _selectWatchDate,
child: InputDecorator(
decoration: const InputDecoration(
labelText: '观看日期',
prefixIcon: Icon(Icons.event),
border: OutlineInputBorder(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_watchDate != null
? '${_watchDate!.year}-${_watchDate!.month.toString().padLeft(2, '0')}-${_watchDate!.day.toString().padLeft(2, '0')}'
: '选择日期',
style: TextStyle(
color: _watchDate != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (_watchDate != null)
IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
setState(() {
_watchDate = null;
});
},
),
],
),
),
),
const SizedBox(height: 16),
// 笔记
TextFormField(
controller: _noteController,
decoration: const InputDecoration(
labelText: '笔记',
hintText: '写下你的观后感...',
prefixIcon: Icon(Icons.edit_note),
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
maxLines: 5,
),
const SizedBox(height: 32),
// 保存按钮
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
onPressed: _saveMovie,
icon: const Icon(Icons.save),
label: Text(isEdit ? '保存修改' : '添加记录'),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
),
),
);
}
/// 选择观看日期
Future<void> _selectWatchDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _watchDate ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_watchDate = picked;
});
}
}
/// 保存影视记录
Future<void> _saveMovie() async {
if (!_formKey.currentState!.validate()) {
return;
}
final year = _yearController.text.isNotEmpty ? int.tryParse(_yearController.text) : null;
final rating = _ratingController.text.isNotEmpty ? double.tryParse(_ratingController.text) : null;
if (widget.movie == null) {
// 添加新模式
final newMovie = Movie(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: _titleController.text.trim(),
year: year,
rating: rating,
status: _status,
watchDate: _watchDate,
note: _noteController.text.trim(),
);
await context.read<AppProvider>().addMovie(newMovie);
} else {
// 编辑现有模式
final updatedMovie = Movie(
id: widget.movie!.id,
title: _titleController.text.trim(),
year: year,
rating: rating,
status: _status,
watchDate: _watchDate,
note: _noteController.text.trim(),
);
await context.read<AppProvider>().updateMovie(updatedMovie);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.movie == null ? '添加成功' : '更新成功'),
behavior: SnackBarBehavior.floating,
),
);
Navigator.pop(context);
}
}

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../widgets/movie_status_bar.dart';
import '../widgets/movie_list_item.dart';
/// 观影标签页
class MovieTabPage extends StatelessWidget {
const MovieTabPage({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 状态选择栏(已看、想看、在看)
const MovieStatusBar(),
// 影片列表
Expanded(
child: _buildMovieList(context),
),
],
);
}
/// 构建影片列表
Widget _buildMovieList(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
// 根据状态筛选影片
final statusMap = {
0: 'watched',
1: 'want_to_watch',
2: 'watching',
};
final currentStatus = statusMap[provider.movieStatusIndex]!;
final movies = provider.getMoviesByStatus(currentStatus);
if (movies.isEmpty) {
return _buildEmptyState(context, provider.movieStatusIndex);
}
return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(),
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: movies.length,
itemBuilder: (context, index) {
return MovieListItem(movie: movies[index]);
},
),
);
},
);
}
/// 构建空状态提示
Widget _buildEmptyState(BuildContext context, int statusIndex) {
final statusText = ['已看', '想看', '在看'][statusIndex];
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.movie_creation_outlined,
size: 80,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'暂无$statusText的影片',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('添加记录'),
onPressed: () {
Navigator.pushNamed(context, '/movie-form');
},
),
],
),
);
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 笔记详情页
class NoteDetailPage extends StatefulWidget {
final Note note;
const NoteDetailPage({super.key, required this.note});
@override
State<NoteDetailPage> createState() => _NoteDetailPageState();
}
class _NoteDetailPageState extends State<NoteDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.note.title),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _navigateToEdit(context),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _showDeleteDialog(context),
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标签区域
if (widget.note.tags.isNotEmpty) _buildTagsSection(context),
// 内容区域
_buildContentSection(context),
// 时间信息
_buildTimeSection(context),
],
),
),
);
}
/// 构建标签区域
Widget _buildTagsSection(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: widget.note.tags.map((tag) {
return Chip(
label: Text(tag),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
labelStyle: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
);
}).toList(),
),
);
}
/// 构建内容区域
Widget _buildContentSection(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.note.content,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
height: 1.8,
),
),
],
),
);
}
/// 构建时间信息区域
Widget _buildTimeSection(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.access_time,
size: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
'创建时间',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
_formatDateTime(widget.note.createdAt),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.edit,
size: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
'更新时间',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
_formatDateTime(widget.note.updatedAt),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
/// 格式化日期时间
String _formatDateTime(DateTime dateTime) {
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/note-form', arguments: widget.note).then((_) {
context.read<AppProvider>().loadNotes();
});
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${widget.note.title}"吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeNote(widget.note.id);
if (!mounted) return;
Navigator.pop(context);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 添加/编辑笔记页面
class NoteFormPage extends StatefulWidget {
final Note? note; // 如果为 null则是添加模式否则是编辑模式
const NoteFormPage({super.key, this.note});
@override
State<NoteFormPage> createState() => _NoteFormPageState();
}
class _NoteFormPageState extends State<NoteFormPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _titleController;
late TextEditingController _contentController;
late TextEditingController _tagsController;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.note?.title ?? '');
_contentController = TextEditingController(text: widget.note?.content ?? '');
_tagsController = TextEditingController(
text: widget.note?.tags.join(', ') ?? '',
);
}
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
_tagsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isEdit = widget.note != null;
return Scaffold(
appBar: AppBar(
title: Text(isEdit ? '编辑笔记' : '添加笔记'),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveNote,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题 *',
hintText: '请输入笔记标题',
prefixIcon: Icon(Icons.title),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入标题';
}
return null;
},
),
const SizedBox(height: 16),
// 标签
TextFormField(
controller: _tagsController,
decoration: const InputDecoration(
labelText: '标签',
hintText: '多个标签用逗号分隔',
prefixIcon: Icon(Icons.local_offer),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 内容
TextFormField(
controller: _contentController,
decoration: const InputDecoration(
labelText: '内容 *',
hintText: '请输入笔记内容...',
prefixIcon: Icon(Icons.edit_note),
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
maxLines: 15,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入内容';
}
return null;
},
),
const SizedBox(height: 32),
// 保存按钮
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
onPressed: _saveNote,
icon: const Icon(Icons.save),
label: Text(isEdit ? '保存修改' : '添加笔记'),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
),
),
);
}
/// 保存笔记
Future<void> _saveNote() async {
if (!_formKey.currentState!.validate()) {
return;
}
// 解析标签
final tags = _tagsController.text
.split(',')
.map((tag) => tag.trim())
.where((tag) => tag.isNotEmpty)
.toList();
if (widget.note == null) {
// 添加新模式
final now = DateTime.now();
final newNote = Note(
id: now.millisecondsSinceEpoch.toString(),
title: _titleController.text.trim(),
content: _contentController.text.trim(),
tags: tags,
createdAt: now,
updatedAt: now,
);
await context.read<AppProvider>().addNote(newNote);
} else {
// 编辑现有模式
final updatedNote = Note(
id: widget.note!.id,
title: _titleController.text.trim(),
content: _contentController.text.trim(),
tags: tags,
createdAt: widget.note!.createdAt,
updatedAt: DateTime.now(),
);
await context.read<AppProvider>().updateNote(updatedNote);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.note == null ? '添加成功' : '更新成功'),
behavior: SnackBarBehavior.floating,
),
);
Navigator.pop(context);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../widgets/note_list_item.dart';
/// 笔记标签页
class NoteTabPage extends StatelessWidget {
const NoteTabPage({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 笔记列表(无状态筛选,显示所有笔记)
Expanded(
child: _buildNoteList(context),
),
],
);
}
/// 构建笔记列表
Widget _buildNoteList(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final notes = provider.notes;
if (notes.isEmpty) {
return _buildEmptyState(context);
}
return RefreshIndicator(
onRefresh: () async => await provider.loadNotes(),
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: notes.length,
itemBuilder: (context, index) {
return NoteListItem(note: notes[index]);
},
),
);
},
);
}
/// 构建空状态提示
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.note_outlined,
size: 80,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'暂无笔记',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('添加笔记'),
onPressed: () {
Navigator.pushNamed(context, '/note-form');
},
),
],
),
);
}
/// 获取示例笔记数据
List<Note> _getSampleNotes() {
// 示例数据(实际应从数据库获取)
return [
Note(
id: '1',
title: 'Flutter 学习心得',
content: '今天开始学习 Flutter 框架,感觉和 Vue 有很多相似之处,都是声明式 UI组件化开发。Widget 的概念很有趣,一切皆 Widget。',
tags: ['学习', 'Flutter', '编程'],
createdAt: DateTime(2024, 3, 1, 10, 30),
updatedAt: DateTime(2024, 3, 1, 10, 30),
),
Note(
id: '2',
title: '《活着》读后感',
content: '余华的《活着》真的是一部让人深思的作品。福贵的一生经历了太多的苦难,但他依然坚强地活着。生命的意义或许就在于活着本身。',
tags: ['阅读', '感悟', '书籍'],
createdAt: DateTime(2024, 2, 20, 15, 20),
updatedAt: DateTime(2024, 2, 20, 16, 0),
),
Note(
id: '3',
title: '电影《星际穿越》观后感',
content: '诺兰的电影总是充满想象力。《星际穿越》将科幻与亲情完美结合,五维空间的呈现方式令人震撼。配乐也是一绝。',
tags: ['观影', '科幻', '电影'],
createdAt: DateTime(2024, 2, 15, 20, 0),
updatedAt: DateTime(2024, 2, 15, 20, 30),
),
Note(
id: '4',
title: 'Python 数据分析笔记',
content: 'Pandas 库的 DataFrame 操作非常强大,可以方便地进行数据清洗和分析。需要多练习熟练掌握常用操作。',
tags: ['Python', '数据分析', '技术'],
createdAt: DateTime(2024, 1, 10, 9, 0),
updatedAt: DateTime(2024, 1, 10, 9, 30),
),
Note(
id: '5',
title: '生活随笔',
content: '春天来了,天气渐暖。周末去公园散步,看到花开得很好。生活中的小确幸值得记录。',
tags: ['生活', '随笔'],
createdAt: DateTime(2024, 3, 3, 18, 0),
updatedAt: DateTime(2024, 3, 3, 18, 0),
),
];
}
}

View File

@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import '../models/data_models.dart';
import '../utils/movie_dao.dart';
import '../utils/book_dao.dart';
import '../utils/note_dao.dart';
/// 应用全局状态管理
class AppProvider extends ChangeNotifier {
// 数据库访问对象
final MovieDao _movieDao = MovieDao();
final BookDao _bookDao = BookDao();
final NoteDao _noteDao = NoteDao();
// 数据列表
List<Movie> _movies = [];
List<Book> _books = [];
List<Note> _notes = [];
// 当前主界面选中的标签 (0: 观影1: 阅读2: 笔记)
int _mainTabIndex = 0;
// 当前底部导航选中的索引 (0: 主页1: 新增2: 我的)
int _bottomNavIndex = 0;
// 观影选中的状态 (0: 已看1: 想看2: 在看)
int _movieStatusIndex = 0;
// 阅读选中的状态 (0: 读完1: 在读2: 准备读)
int _bookStatusIndex = 0;
// 侧边菜单是否打开
bool _drawerOpen = false;
// 初始化数据库
Future<void> initDatabase() async {
await loadMovies();
await loadBooks();
await loadNotes();
}
// 加载影视数据
Future<void> loadMovies() async {
_movies = await _movieDao.getAllMovies();
notifyListeners();
}
// 加载书籍数据
Future<void> loadBooks() async {
_books = await _bookDao.getAllBooks();
notifyListeners();
}
// 加载笔记数据
Future<void> loadNotes() async {
_notes = await _noteDao.getAllNotes();
notifyListeners();
}
// Getters
int get mainTabIndex => _mainTabIndex;
int get bottomNavIndex => _bottomNavIndex;
int get movieStatusIndex => _movieStatusIndex;
int get bookStatusIndex => _bookStatusIndex;
bool get drawerOpen => _drawerOpen;
List<Movie> get movies => _movies;
List<Book> get books => _books;
List<Note> get notes => _notes;
// 根据状态获取影视列表
List<Movie> getMoviesByStatus(String status) {
return _movies.where((movie) => movie.status == status).toList();
}
// 根据状态获取书籍列表
List<Book> getBooksByStatus(String status) {
return _books.where((book) => book.status == status).toList();
}
// Setters
void setMainTabIndex(int index) {
_mainTabIndex = index;
notifyListeners();
}
void setBottomNavIndex(int index) {
_bottomNavIndex = index;
notifyListeners();
}
void setMovieStatusIndex(int index) {
_movieStatusIndex = index;
notifyListeners();
}
void setBookStatusIndex(int index) {
_bookStatusIndex = index;
notifyListeners();
}
void toggleDrawer() {
_drawerOpen = !_drawerOpen;
notifyListeners();
}
void closeDrawer() {
_drawerOpen = false;
notifyListeners();
}
// 添加影视记录
Future<void> addMovie(Movie movie) async {
await _movieDao.insertMovie(movie);
await loadMovies();
}
// 更新影视记录
Future<void> updateMovie(Movie movie) async {
await _movieDao.updateMovie(movie);
await loadMovies();
}
// 删除影视记录
Future<void> removeMovie(String id) async {
await _movieDao.deleteMovie(id);
await loadMovies();
}
// 添加书籍记录
Future<void> addBook(Book book) async {
await _bookDao.insertBook(book);
await loadBooks();
}
// 更新书籍记录
Future<void> updateBook(Book book) async {
await _bookDao.updateBook(book);
await loadBooks();
}
// 删除书籍记录
Future<void> removeBook(String id) async {
await _bookDao.deleteBook(id);
await loadBooks();
}
// 添加笔记
Future<void> addNote(Note note) async {
await _noteDao.insertNote(note);
await loadNotes();
}
// 更新笔记
Future<void> updateNote(Note note) async {
await _noteDao.updateNote(note);
await loadNotes();
}
// 删除笔记
Future<void> removeNote(String id) async {
await _noteDao.deleteNote(id);
await loadNotes();
}
}

60
lib/utils/app_router.dart Normal file
View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import '../models/data_models.dart';
import '../pages/movie_form_page.dart';
import '../pages/book_form_page.dart';
import '../pages/note_form_page.dart';
import '../pages/movie_detail_page.dart';
import '../pages/book_detail_page.dart';
import '../pages/note_detail_page.dart';
/// 路由生成器
class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case '/movie-form':
final movie = settings.arguments as Movie?;
return MaterialPageRoute(
builder: (_) => MovieFormPage(movie: movie),
);
case '/book-form':
final book = settings.arguments as Book?;
return MaterialPageRoute(
builder: (_) => BookFormPage(book: book),
);
case '/note-form':
final note = settings.arguments as Note?;
return MaterialPageRoute(
builder: (_) => NoteFormPage(note: note),
);
case '/movie-detail':
final movie = settings.arguments as Movie;
return MaterialPageRoute(
builder: (_) => MovieDetailPage(movie: movie),
);
case '/book-detail':
final book = settings.arguments as Book;
return MaterialPageRoute(
builder: (_) => BookDetailPage(book: book),
);
case '/note-detail':
final note = settings.arguments as Note;
return MaterialPageRoute(
builder: (_) => NoteDetailPage(note: note),
);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('未找到页面:${settings.name}'),
),
),
);
}
}
}

66
lib/utils/app_theme.dart Normal file
View File

@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class AppTheme {
// 主色调
static const Color primaryColor = Color(0xFF6200EE);
static const Color secondaryColor = Color(0xFF03DAC6);
// 状态颜色
static const Color watchedColor = Color(0xFF4CAF50); // 已看 - 绿色
static const Color wantToWatchColor = Color(0xFFFF9800); // 想看 - 橙色
static const Color watchingColor = Color(0xFF2196F3); // 在看 - 蓝色
static const Color readColor = Color(0xFF4CAF50); // 读完 - 绿色
static const Color wantToReadColor = Color(0xFFFF9800); // 准备读 - 橙色
static const Color readingColor = Color(0xFF2196F3); // 在读 - 蓝色
// 亮色主题
static final ThemeData lightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
),
appBarTheme: const AppBarTheme(
centerTitle: false,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
elevation: 8,
selectedItemColor: primaryColor,
unselectedItemColor: Colors.grey,
),
);
// 暗色主题
static final ThemeData darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
),
appBarTheme: const AppBarTheme(
centerTitle: false,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
elevation: 8,
selectedItemColor: secondaryColor,
unselectedItemColor: Colors.grey,
),
);
}

81
lib/utils/book_dao.dart Normal file
View File

@@ -0,0 +1,81 @@
import 'package:sqflite/sqflite.dart';
import '../models/data_models.dart';
import 'database_helper.dart';
/// 书籍数据访问对象
class BookDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// 获取所有书籍记录
Future<List<Book>> getAllBooks() async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('books');
return List.generate(maps.length, (i) {
return Book(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
author: maps[i]['author'],
cover: maps[i]['cover'],
rating: maps[i]['rating']?.toDouble(),
status: maps[i]['status'],
readDate: maps[i]['read_date'] != null
? DateTime.parse(maps[i]['read_date'])
: null,
note: maps[i]['note'],
);
});
}
// 根据状态筛选书籍记录
Future<List<Book>> getBooksByStatus(String status) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
where: 'status = ?',
whereArgs: [status],
);
return List.generate(maps.length, (i) {
return Book(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
author: maps[i]['author'],
cover: maps[i]['cover'],
rating: maps[i]['rating']?.toDouble(),
status: maps[i]['status'],
readDate: maps[i]['read_date'] != null
? DateTime.parse(maps[i]['read_date'])
: null,
note: maps[i]['note'],
);
});
}
// 添加书籍记录
Future<int> insertBook(Book book) async {
final db = await _dbHelper.database;
return await db.insert('books', book.toJson());
}
// 更新书籍记录
Future<int> updateBook(Book book) async {
final db = await _dbHelper.database;
return await db.update(
'books',
book.toJson(),
where: 'id = ?',
whereArgs: [book.id],
);
}
// 删除书籍记录
Future<int> deleteBook(String id) async {
final db = await _dbHelper.database;
return await db.delete(
'books',
where: 'id = ?',
whereArgs: [id],
);
}
}

View File

@@ -0,0 +1,81 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
/// 数据库帮助类 - 管理数据库的创建和版本控制
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('mooknote.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}
// 创建数据库表
Future<void> _createDB(Database db, int version) async {
const idType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
const textType = 'TEXT NOT NULL';
const integerType = 'INTEGER NOT NULL';
const booleanType = 'INTEGER NOT NULL';
// 影视表
await db.execute('''
CREATE TABLE movies (
id $idType,
title $textType,
poster TEXT,
rating REAL,
year INTEGER,
status $textType,
watch_date TEXT,
note TEXT
)
''');
// 书籍表
await db.execute('''
CREATE TABLE books (
id $idType,
title $textType,
author TEXT,
cover TEXT,
rating REAL,
status $textType,
read_date TEXT,
note TEXT
)
''');
// 笔记表
await db.execute('''
CREATE TABLE notes (
id $idType,
title $textType,
content $textType,
tags TEXT,
created_at $textType,
updated_at $textType
)
''');
}
// 关闭数据库
Future close() async {
final db = await instance.database;
db.close();
}
}

81
lib/utils/movie_dao.dart Normal file
View File

@@ -0,0 +1,81 @@
import 'package:sqflite/sqflite.dart';
import '../models/data_models.dart';
import 'database_helper.dart';
/// 影视数据访问对象
class MovieDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// 获取所有影视记录
Future<List<Movie>> getAllMovies() async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('movies');
return List.generate(maps.length, (i) {
return Movie(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
poster: maps[i]['poster'],
rating: maps[i]['rating']?.toDouble(),
year: maps[i]['year'],
status: maps[i]['status'],
watchDate: maps[i]['watch_date'] != null
? DateTime.parse(maps[i]['watch_date'])
: null,
note: maps[i]['note'],
);
});
}
// 根据状态筛选影视记录
Future<List<Movie>> getMoviesByStatus(String status) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'movies',
where: 'status = ?',
whereArgs: [status],
);
return List.generate(maps.length, (i) {
return Movie(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
poster: maps[i]['poster'],
rating: maps[i]['rating']?.toDouble(),
year: maps[i]['year'],
status: maps[i]['status'],
watchDate: maps[i]['watch_date'] != null
? DateTime.parse(maps[i]['watch_date'])
: null,
note: maps[i]['note'],
);
});
}
// 添加影视记录
Future<int> insertMovie(Movie movie) async {
final db = await _dbHelper.database;
return await db.insert('movies', movie.toJson());
}
// 更新影视记录
Future<int> updateMovie(Movie movie) async {
final db = await _dbHelper.database;
return await db.update(
'movies',
movie.toJson(),
where: 'id = ?',
whereArgs: [movie.id],
);
}
// 删除影视记录
Future<int> deleteMovie(String id) async {
final db = await _dbHelper.database;
return await db.delete(
'movies',
where: 'id = ?',
whereArgs: [id],
);
}
}

69
lib/utils/note_dao.dart Normal file
View File

@@ -0,0 +1,69 @@
import 'package:sqflite/sqflite.dart';
import '../models/data_models.dart';
import 'database_helper.dart';
/// 笔记数据访问对象
class NoteDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// 获取所有笔记
Future<List<Note>> getAllNotes() async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'notes',
orderBy: 'updated_at DESC',
);
return List.generate(maps.length, (i) {
return Note(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
content: maps[i]['content'],
tags: maps[i]['tags'] != null
? List<String>.from(maps[i]['tags'].split(','))
: [],
createdAt: DateTime.parse(maps[i]['created_at']),
updatedAt: DateTime.parse(maps[i]['updated_at']),
);
});
}
// 添加笔记
Future<int> insertNote(Note note) async {
final db = await _dbHelper.database;
return await db.insert('notes', {
'id': note.id,
'title': note.title,
'content': note.content,
'tags': note.tags.join(','),
'created_at': note.createdAt.toIso8601String(),
'updated_at': note.updatedAt.toIso8601String(),
});
}
// 更新笔记
Future<int> updateNote(Note note) async {
final db = await _dbHelper.database;
return await db.update(
'notes',
{
'title': note.title,
'content': note.content,
'tags': note.tags.join(','),
'updated_at': note.updatedAt.toIso8601String(),
},
where: 'id = ?',
whereArgs: [note.id],
);
}
// 删除笔记
Future<int> deleteNote(String id) async {
final db = await _dbHelper.database;
return await db.delete(
'notes',
where: 'id = ?',
whereArgs: [id],
);
}
}

View File

@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 书籍列表项组件
class BookListItem extends StatelessWidget {
final Book book;
const BookListItem({super.key, required this.book});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
// 跳转到详情页
Navigator.pushNamed(context, '/book-detail', arguments: book);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面占位图
_buildCover(),
const SizedBox(width: 12),
// 书籍信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
book.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (book.author != null) ...[
const SizedBox(height: 4),
Text(
book.author!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(height: 8),
// 评分和状态
Row(
children: [
if (book.rating != null) ...[
Icon(
Icons.star,
size: 16,
color: Colors.amber[700],
),
const SizedBox(width: 4),
Text(
book.rating.toString(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.amber[700],
),
),
const SizedBox(width: 12),
],
// 状态标签
_buildStatusTag(context),
],
),
if (book.readDate != null) ...[
const SizedBox(height: 6),
Text(
'阅读日期:${_formatDate(book.readDate!)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
if (book.note != null && book.note!.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
book.note!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// 右侧操作按钮
Column(
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: () {
Navigator.pushNamed(context, '/book-form', arguments: book);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context, book),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
),
),
);
}
/// 构建封面占位图
Widget _buildCover() {
return Container(
width: 60,
height: 90,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.menu_book,
color: Colors.grey[500],
size: 32,
),
);
}
/// 构建状态标签
Widget _buildStatusTag(BuildContext context) {
Color statusColor;
String statusText;
switch (book.status) {
case 'read':
statusColor = AppTheme.readColor;
statusText = '读完';
break;
case 'reading':
statusColor = AppTheme.readingColor;
statusText = '在读';
break;
case 'want_to_read':
statusColor = AppTheme.wantToReadColor;
statusText = '准备读';
break;
default:
statusColor = Colors.grey;
statusText = '未知';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: statusColor,
width: 1,
),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 11,
color: statusColor,
fontWeight: FontWeight.bold,
),
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context, Book book) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${book.title}"吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeBook(book.id);
if (!context.mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../utils/app_theme.dart';
/// 阅读状态选择栏
class BookStatusBar extends StatelessWidget {
const BookStatusBar({super.key});
@override
Widget build(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
_buildStatusItem(
context,
'读完',
AppTheme.readColor,
Icons.check_circle,
0,
provider.bookStatusIndex,
() => provider.setBookStatusIndex(0),
),
const SizedBox(width: 12),
_buildStatusItem(
context,
'在读',
AppTheme.readingColor,
Icons.auto_stories,
1,
provider.bookStatusIndex,
() => provider.setBookStatusIndex(1),
),
const SizedBox(width: 12),
_buildStatusItem(
context,
'准备读',
AppTheme.wantToReadColor,
Icons.bookmark_border,
2,
provider.bookStatusIndex,
() => provider.setBookStatusIndex(2),
),
],
),
);
},
);
}
/// 构建状态项
Widget _buildStatusItem(
BuildContext context,
String label,
Color color,
IconData icon,
int index,
int currentIndex,
VoidCallback onTap,
) {
final isSelected = index == currentIndex;
return Expanded(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? color.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
/// 自定义底部导航栏
class CustomBottomNavBar extends StatelessWidget {
const CustomBottomNavBar({super.key});
@override
Widget build(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return BottomNavigationBar(
currentIndex: provider.bottomNavIndex,
onTap: (index) {
if (index == 1) {
// 新增按钮 - 显示选择对话框
_showAddDialog(context, provider);
} else {
provider.setBottomNavIndex(index);
if (index == 0) {
// 主页 - 重置到首页
provider.setMainTabIndex(0);
}
// index == 2 是我的页面(待实现)
}
},
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: '主页',
),
BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline),
activeIcon: Icon(Icons.add_circle),
label: '新增',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: '我的',
),
],
);
},
);
}
/// 显示新增对话框
void _showAddDialog(BuildContext context, AppProvider provider) {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.movie, color: Colors.green),
title: const Text('添加观影'),
subtitle: const Text('记录你看过的电影'),
onTap: () {
Navigator.pop(context);
// 直接打开添加观影表单
Navigator.pushNamed(context, '/movie-form');
},
),
ListTile(
leading: const Icon(Icons.menu_book, color: Colors.orange),
title: const Text('添加阅读'),
subtitle: const Text('记录你读过的书'),
onTap: () {
Navigator.pop(context);
// 直接打开添加阅读表单
Navigator.pushNamed(context, '/book-form');
},
),
ListTile(
leading: const Icon(Icons.note, color: Colors.blue),
title: const Text('添加笔记'),
subtitle: const Text('记录你的想法和笔记'),
onTap: () {
Navigator.pop(context);
// 直接打开添加笔记表单
Navigator.pushNamed(context, '/note-form');
},
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
/// 自定义左侧弹出菜单
class CustomDrawer extends StatelessWidget {
const CustomDrawer({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Drawer(
child: Column(
children: [
// 顶部用户信息区域(含热力图)
_buildHeader(context),
// 分割线
Divider(height: 1, color: colorScheme.outlineVariant),
// 菜单项列表
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: const Icon(Icons.analytics),
title: const Text('统计'),
onTap: () {
Navigator.pop(context);
// TODO: 跳转到统计页面
},
),
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('回收站'),
onTap: () {
Navigator.pop(context);
// TODO: 跳转到回收站页面
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('设置'),
onTap: () {
Navigator.pop(context);
// TODO: 跳转到设置页面
},
),
],
),
),
// 底部版本信息
Container(
padding: const EdgeInsets.all(16),
child: Text(
'MookNote v1.0.0',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
);
}
/// 构建头部(含热力图)
Widget _buildHeader(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.surface,
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 用户头像和名称
Row(
children: [
CircleAvatar(
radius: 32,
backgroundColor: Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.person,
color: Colors.white,
size: 32,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'用户',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'记录生活点滴',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
const SizedBox(height: 20),
// GitHub 风格热力图
_buildHeatmap(context),
],
),
);
},
);
}
/// 构建热力图
Widget _buildHeatmap(BuildContext context) {
const int weeks = 52; // 一年 52 周
const int daysPerWeek = 7;
// 生成随机数据(实际应从数据库获取)
final random = DateTime.now().millisecondsSinceEpoch;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'年度记录',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
SizedBox(
height: daysPerWeek * 14, // 每个格子 14x14
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(weeks, (weekIndex) {
return Padding(
padding: const EdgeInsets.only(right: 2),
child: Column(
children: List.generate(daysPerWeek, (dayIndex) {
// 根据随机值决定颜色深度
final intensity = (random + weekIndex * 7 + dayIndex) % 100 / 100;
final color = _getHeatmapColor(intensity, context);
return Container(
margin: const EdgeInsets.only(bottom: 2),
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
);
}),
),
);
}),
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Less',
style: Theme.of(context).textTheme.labelSmall,
),
Text(
'More',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
],
);
}
/// 根据强度获取热力图颜色
Color _getHeatmapColor(double intensity, BuildContext context) {
if (intensity == 0) {
return Theme.of(context).colorScheme.surfaceContainerHighest;
} else if (intensity < 0.25) {
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.4);
} else if (intensity < 0.5) {
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6);
} else if (intensity < 0.75) {
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8);
} else {
return Theme.of(context).colorScheme.primary;
}
}
}

View File

@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 观影列表项组件
class MovieListItem extends StatelessWidget {
final Movie movie;
const MovieListItem({super.key, required this.movie});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
// 跳转到详情页
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 海报占位图
_buildPoster(),
const SizedBox(width: 12),
// 影片信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和年份
Text(
movie.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (movie.year != null) ...[
const SizedBox(height: 4),
Text(
'${movie.year}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(height: 8),
// 评分和状态
Row(
children: [
if (movie.rating != null) ...[
Icon(
Icons.star,
size: 16,
color: Colors.amber[700],
),
const SizedBox(width: 4),
Text(
movie.rating.toString(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.amber[700],
),
),
const SizedBox(width: 12),
],
// 状态标签
_buildStatusTag(context),
],
),
if (movie.watchDate != null) ...[
const SizedBox(height: 6),
Text(
'观看日期:${_formatDate(movie.watchDate!)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
if (movie.note != null && movie.note!.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
movie.note!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// 右侧操作按钮
Column(
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: () {
Navigator.pushNamed(context, '/movie-form', arguments: movie);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context, movie),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
),
),
);
}
/// 构建海报占位图
Widget _buildPoster() {
return Container(
width: 60,
height: 90,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.movie,
color: Colors.grey[500],
size: 32,
),
);
}
/// 构建状态标签
Widget _buildStatusTag(BuildContext context) {
Color statusColor;
String statusText;
switch (movie.status) {
case 'watched':
statusColor = AppTheme.watchedColor;
statusText = '已看';
break;
case 'want_to_watch':
statusColor = AppTheme.wantToWatchColor;
statusText = '想看';
break;
case 'watching':
statusColor = AppTheme.watchingColor;
statusText = '在看';
break;
default:
statusColor = Colors.grey;
statusText = '未知';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: statusColor,
width: 1,
),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 11,
color: statusColor,
fontWeight: FontWeight.bold,
),
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context, Movie movie) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${movie.title}"吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMovie(movie.id);
if (!context.mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../utils/app_theme.dart';
/// 观影状态选择栏
class MovieStatusBar extends StatelessWidget {
const MovieStatusBar({super.key});
@override
Widget build(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
_buildStatusItem(
context,
'已看',
AppTheme.watchedColor,
Icons.check_circle,
0,
provider.movieStatusIndex,
() => provider.setMovieStatusIndex(0),
),
const SizedBox(width: 12),
_buildStatusItem(
context,
'想看',
AppTheme.wantToWatchColor,
Icons.bookmark_border,
1,
provider.movieStatusIndex,
() => provider.setMovieStatusIndex(1),
),
const SizedBox(width: 12),
_buildStatusItem(
context,
'在看',
AppTheme.watchingColor,
Icons.play_circle_outline,
2,
provider.movieStatusIndex,
() => provider.setMovieStatusIndex(2),
),
],
),
);
},
);
}
/// 构建状态项
Widget _buildStatusItem(
BuildContext context,
String label,
Color color,
IconData icon,
int index,
int currentIndex,
VoidCallback onTap,
) {
final isSelected = index == currentIndex;
return Expanded(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? color.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 笔记列表项组件
class NoteListItem extends StatelessWidget {
final Note note;
const NoteListItem({super.key, required this.note});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
// 跳转到详情页
Navigator.pushNamed(context, '/note-detail', arguments: note);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
note.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// 内容摘要
Text(
note.content,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// 标签
if (note.tags.isNotEmpty) ...[
Wrap(
spacing: 6,
runSpacing: 6,
children: note.tags.map((tag) => _buildTag(context, tag)).toList(),
),
],
const SizedBox(height: 12),
// 时间信息和操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDate(note.updatedAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: () {
Navigator.pushNamed(context, '/note-form', arguments: note);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context, note),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
],
),
),
),
);
}
/// 构建标签
Widget _buildTag(BuildContext context, String tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'#$tag',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
if (difference.inHours == 0) {
if (difference.inMinutes == 0) {
return '刚刚';
}
return '${difference.inMinutes}分钟前';
}
return '${difference.inHours}小时前';
} else if (difference.inDays < 7) {
return '${difference.inDays}天前';
} else {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context, Note note) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${note.title}"吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeNote(note.id);
if (!context.mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}

309
pubspec.lock Normal file
View File

@@ -0,0 +1,309 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
url: "https://pub.dev"
source: hosted
version: "0.69.2"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
url: "https://pub.dev"
source: hosted
version: "2.4.2+2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.9"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.24.0"

27
pubspec.yaml Normal file
View File

@@ -0,0 +1,27 @@
name: mooknote
description: "A Flutter application for tracking movies, books, and notes"
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.5.0
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
provider: ^6.1.2
fl_chart: ^0.69.0
sqflite: ^2.3.0
path: ^1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true
assets:
- assets/images/

30
test/widget_test.dart Normal file
View File

@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mooknote/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}