Compare commits
2 Commits
6926c78bef
...
34e91fbd70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34e91fbd70 | ||
|
|
2b956eb96c |
@ -2,7 +2,10 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(flutter analyze:*)",
|
"Bash(flutter analyze:*)",
|
||||||
"Bash(flutter run:*)"
|
"Bash(flutter run:*)",
|
||||||
|
"Bash(tree:*)",
|
||||||
|
"Bash(flutter pub get:*)",
|
||||||
|
"Bash(sed:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
391
CLAUDE.md
391
CLAUDE.md
@ -1,122 +1,339 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
此文件为 Claude Code (claude.ai/code) 提供本仓库代码开发的指导说明。
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## 项目概览
|
## 项目概览
|
||||||
|
|
||||||
**SnapWish** - 一款使用中文界面的 Flutter 照片管理应用。该应用允许用户将照片组织到分类中、添加标签并管理他们的照片收藏。
|
**项目名称**:想拍 (InspoSnap)
|
||||||
|
**项目描述**:Shoot What Inspires You - 拍摄灵感收集与管理应用
|
||||||
|
**当前阶段**:MVP v1.1 开发中
|
||||||
|
**技术栈**:Flutter + Riverpod + Hive
|
||||||
|
**参考文档**:[snapwish_PRD_v1.1.md](snapwish_PRD_v1.1.md) - 已确认的产品需求文档
|
||||||
|
|
||||||
## 架构结构
|
## 技术方案
|
||||||
|
|
||||||
该应用采用标准的 Flutter Material Design 架构:
|
### 核心技术选型
|
||||||
- **主入口**: `lib/main.dart` - 包含根组件和导航设置
|
- **框架**:Flutter 3.2.5+ (Dart)
|
||||||
- **导航**: 底部导航栏包含 2 个标签页(照片、分类)
|
- **状态管理**:Riverpod (现代、类型安全)
|
||||||
- **悬浮按钮**: 通用添加照片按钮
|
- **数据库**:Hive (轻量级、高性能)
|
||||||
- **页面**: 位于 `lib/pages/` 目录
|
- **图片处理**:flutter_image_compress + image
|
||||||
|
- **瀑布流**:flutter_staggered_grid_view
|
||||||
|
- **国际化**:flutter_localizations
|
||||||
|
- **分享接收**:receive_sharing_intent
|
||||||
|
- **图片查看**:photo_view
|
||||||
|
- **UI组件**:Material Design + 自定义主题
|
||||||
|
|
||||||
## 核心组件
|
### 架构设计
|
||||||
|
```
|
||||||
### 主结构 (`lib/main.dart`)
|
lib/
|
||||||
- **SnapWishApp**: 根 MaterialApp 组件,包含主题配置
|
├── core/ # 核心功能
|
||||||
- **MainPage**: 管理底部导航的有状态组件
|
│ ├── constants/ # 常量定义
|
||||||
- **路由**: `/add-photo` 路由用于添加新照片
|
│ ├── errors/ # 错误处理
|
||||||
|
│ ├── utils/ # 工具类(图片压缩、文件操作)
|
||||||
### 页面 (`lib/pages/`)
|
│ └── theme/ # 主题配置
|
||||||
- **PhotoPage** (`photo_page.dart`): 照片网格显示和照片详情查看器
|
├── data/ # 数据层
|
||||||
- **CategoriesPage** (`categories_page.dart`): 文件夹/分类管理,包含创建/删除功能
|
│ ├── datasources/ # 数据源
|
||||||
- **AddPhotoPage** (`add_photo_page.dart`): 照片上传表单,包含分类选择和标签功能
|
│ │ ├── local/ # Hive数据库
|
||||||
|
│ │ └── share/ # 分享接收
|
||||||
|
│ ├── models/ # 数据模型
|
||||||
|
│ └── repositories/ # 数据仓库
|
||||||
|
├── domain/ # 业务逻辑层
|
||||||
|
│ ├── entities/ # 实体类
|
||||||
|
│ ├── repositories/ # 仓库接口
|
||||||
|
│ └── usecases/ # 用例
|
||||||
|
├── presentation/ # 表示层
|
||||||
|
│ ├── providers/ # Riverpod状态管理
|
||||||
|
│ ├── pages/ # 页面
|
||||||
|
│ ├── widgets/ # 组件
|
||||||
|
│ └── l10n/ # 国际化
|
||||||
|
└── main.dart # 应用入口
|
||||||
|
```
|
||||||
|
|
||||||
## 开发命令
|
## 开发命令
|
||||||
|
|
||||||
### 构建与运行
|
|
||||||
```bash
|
```bash
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
flutter pub get
|
flutter pub get
|
||||||
|
|
||||||
# 运行开发服务器
|
# 运行应用
|
||||||
flutter run
|
flutter run # 默认设备
|
||||||
|
flutter run -d android # Android设备
|
||||||
# 构建 APK
|
flutter run -d ios # iOS设备
|
||||||
flutter build apk
|
|
||||||
|
|
||||||
# 构建 iOS 应用
|
|
||||||
flutter build ios
|
|
||||||
|
|
||||||
# 构建 Web 应用
|
|
||||||
flutter build web
|
|
||||||
```
|
|
||||||
|
|
||||||
### 测试与代码质量
|
|
||||||
```bash
|
|
||||||
# 运行所有测试
|
|
||||||
flutter test
|
|
||||||
|
|
||||||
# 运行特定测试文件
|
|
||||||
flutter test test/widget_test.dart
|
|
||||||
|
|
||||||
# 代码分析
|
# 代码分析
|
||||||
flutter analyze
|
flutter analyze
|
||||||
|
|
||||||
# 代码格式化
|
# 运行测试
|
||||||
flutter format .
|
flutter test
|
||||||
|
|
||||||
|
# 构建发布
|
||||||
|
flutter build apk # Android APK
|
||||||
|
flutter build ios # iOS应用
|
||||||
|
flutter build web # Web版本
|
||||||
|
|
||||||
|
# 清理缓存
|
||||||
|
flutter clean
|
||||||
```
|
```
|
||||||
|
|
||||||
### 开发工作流
|
## 技术规范
|
||||||
```bash
|
|
||||||
# 开发期间热重载
|
|
||||||
flutter run --hot
|
|
||||||
|
|
||||||
# 检查过时包
|
### 图片处理规范
|
||||||
flutter pub outdated
|
- **缩略图**:长边500px,WebP格式,85%质量,保持原始比例
|
||||||
|
- **大GIF处理**:提取前几帧作为缩略图,避免内存溢出
|
||||||
|
- **存储路径**:按保存日期分类 `/yyyy/MM/dd/`
|
||||||
|
- **缓存策略**:30天/500MB自动清理
|
||||||
|
|
||||||
# 升级包
|
### 性能指标
|
||||||
flutter pub upgrade
|
- **冷启动时间**:< 2秒
|
||||||
|
- **图片加载空窗期**:< 200ms
|
||||||
|
- **搜索响应时间**:< 500ms
|
||||||
|
- **内存占用**:< 200MB(正常使用)
|
||||||
|
- **用户操作响应**:< 200ms
|
||||||
|
|
||||||
|
### UI/UX规范
|
||||||
|
- **响应式断点**:手机≤600dp(2列),平板>600dp(3-4列)
|
||||||
|
- **图片间距**:8dp标准间距
|
||||||
|
- **动画时长**:页面转场300ms
|
||||||
|
- **加载体验**:骨架屏 + 进度提示
|
||||||
|
|
||||||
|
## 开发任务分解
|
||||||
|
|
||||||
|
### 📋 任务清单(模块化开发)
|
||||||
|
|
||||||
|
#### 🔧 Phase 1: 架构搭建(3-4天)
|
||||||
|
**任务1.1: 项目基础配置**
|
||||||
|
- [ ] 创建Flutter项目并配置基础依赖
|
||||||
|
- [ ] 配置国际化支持(简中/繁中/English)
|
||||||
|
- [ ] 设置主题系统(浅色/深色/跟随系统)
|
||||||
|
- [ ] 配置Material Design和自定义色彩方案
|
||||||
|
|
||||||
|
**任务1.2: 数据层架构**
|
||||||
|
- [ ] 配置Hive数据库和实体模型
|
||||||
|
- [ ] 实现数据访问对象(DAO)模式
|
||||||
|
- [ ] 创建基础Repository接口和实现
|
||||||
|
- [ ] 设置数据迁移和版本管理
|
||||||
|
|
||||||
|
**任务1.3: 核心工具类**
|
||||||
|
- [ ] 图片压缩工具类(长边500px + WebP)
|
||||||
|
- [ ] 文件存储管理工具
|
||||||
|
- [ ] UUID生成和路径管理
|
||||||
|
- [ ] 错误处理和日志系统
|
||||||
|
|
||||||
|
**任务1.4: 基础UI组件**
|
||||||
|
- [ ] 创建可复用的按钮组件
|
||||||
|
- [ ] 实现加载状态组件(骨架屏)
|
||||||
|
- [ ] 空状态页面组件
|
||||||
|
- [ ] 错误提示组件
|
||||||
|
|
||||||
|
#### 📤 Phase 2: 分享功能(2-3天)
|
||||||
|
**任务2.1: 分享接收机制**
|
||||||
|
- [ ] 配置receive_sharing_intent插件
|
||||||
|
- [ ] 实现Android分享接收配置
|
||||||
|
- [ ] 实现iOS分享接收配置
|
||||||
|
- [ ] 处理多张图片接收逻辑
|
||||||
|
|
||||||
|
**任务2.2: 保存界面UI**
|
||||||
|
- [ ] 创建半透明模态框界面
|
||||||
|
- [ ] 实现图片网格预览组件
|
||||||
|
- [ ] 构建文件夹选择器弹窗
|
||||||
|
- [ ] 实现标签输入和选择组件
|
||||||
|
|
||||||
|
**任务2.3: 保存业务逻辑**
|
||||||
|
- [ ] 实现批量保存模式(默认)
|
||||||
|
- [ ] 实现单张编辑模式切换
|
||||||
|
- [ ] 处理保存进度提示
|
||||||
|
- [ ] 异步保存和缩略图生成
|
||||||
|
|
||||||
|
**任务2.4: 文件夹管理**
|
||||||
|
- [ ] 创建文件夹弹窗界面
|
||||||
|
- [ ] 实现Material Icons选择器
|
||||||
|
- [ ] 文件夹排序(最近使用)
|
||||||
|
- [ ] 文件夹CRUD操作
|
||||||
|
|
||||||
|
#### 🖼️ Phase 3: 图库展示(2-3天)
|
||||||
|
**任务3.1: 主图库界面**
|
||||||
|
- [ ] 实现瀑布流布局(flutter_staggered_grid_view)
|
||||||
|
- [ ] 响应式列数切换(手机2列/平板3-4列)
|
||||||
|
- [ ] 图片懒加载和缓存优化
|
||||||
|
- [ ] 下拉刷新和上拉加载更多
|
||||||
|
|
||||||
|
**任务3.2: 搜索功能**
|
||||||
|
- [ ] 实现常驻搜索框
|
||||||
|
- [ ] 模糊搜索算法(文件夹+标签+备注)
|
||||||
|
- [ ] 搜索历史记录(最近10条)
|
||||||
|
- [ ] 空搜索结果页面
|
||||||
|
|
||||||
|
**任务3.3: 状态管理**
|
||||||
|
- [ ] 图库状态管理(Riverpod)
|
||||||
|
- [ ] 图片加载状态管理
|
||||||
|
- [ ] 搜索状态管理
|
||||||
|
- [ ] 分页数据管理
|
||||||
|
|
||||||
|
#### 📁 Phase 4: 管理功能(3-4天)
|
||||||
|
**任务4.1: 图片详情页**
|
||||||
|
- [ ] 图片查看器(photo_view)
|
||||||
|
- [ ] 左右滑动切换(非循环)
|
||||||
|
- [ ] 双击放大/缩小功能
|
||||||
|
- [ ] 横竖屏适配
|
||||||
|
|
||||||
|
**任务4.2: 详情信息展示**
|
||||||
|
- [ ] 显示文件夹、标签、备注信息
|
||||||
|
- [ ] 编辑功能入口
|
||||||
|
- [ ] 删除功能(二次确认)
|
||||||
|
- [ ] 导出到系统相册
|
||||||
|
|
||||||
|
**任务4.3: 标签管理系统**
|
||||||
|
- [ ] 标签列表页面
|
||||||
|
- [ ] 标签编辑功能
|
||||||
|
- [ ] Material Icons选择
|
||||||
|
- [ ] 标签使用统计
|
||||||
|
|
||||||
|
**任务4.4: 文件夹页面**
|
||||||
|
- [ ] 文件夹网格展示
|
||||||
|
- [ ] 文件夹封面设置
|
||||||
|
- [ ] 文件夹重命名功能
|
||||||
|
- [ ] 文件夹删除处理
|
||||||
|
|
||||||
|
#### ⚙️ Phase 5: 搜索设置(2-3天)
|
||||||
|
**任务5.1: 设置页面**
|
||||||
|
- [ ] 设置页面分组布局
|
||||||
|
- [ ] 语言切换功能
|
||||||
|
- [ ] 主题模式切换
|
||||||
|
- [ ] 网格布局切换
|
||||||
|
|
||||||
|
**任务5.2: 存储管理**
|
||||||
|
- [ ] 存储使用情况显示
|
||||||
|
- [ ] 一键清理缓存功能
|
||||||
|
- [ ] 缩略图管理
|
||||||
|
- [ ] 存储路径管理
|
||||||
|
|
||||||
|
**任务5.3: 性能优化**
|
||||||
|
- [ ] 虚拟滚动实现
|
||||||
|
- [ ] 内存管理优化
|
||||||
|
- [ ] 大图片处理优化
|
||||||
|
- [ ] 启动速度优化
|
||||||
|
|
||||||
|
**任务5.4: 最终调试**
|
||||||
|
- [ ] 完整功能测试
|
||||||
|
- [ ] 性能测试和调优
|
||||||
|
- [ ] 多设备兼容性测试
|
||||||
|
- [ ] Bug修复和完善
|
||||||
|
|
||||||
|
## 数据模型定义
|
||||||
|
|
||||||
|
### 核心实体类
|
||||||
|
```dart
|
||||||
|
// 图片实体
|
||||||
|
class InspirationImage {
|
||||||
|
final String id; // UUID
|
||||||
|
final String filePath; // 原图路径
|
||||||
|
final String thumbnailPath; // 缩略图路径
|
||||||
|
final String? folderId; // 文件夹ID
|
||||||
|
final List<String> tags; // 标签ID列表
|
||||||
|
final String? note; // 备注内容
|
||||||
|
final DateTime createdAt; // 创建时间(保存时间)
|
||||||
|
final DateTime updatedAt; // 更新时间
|
||||||
|
final String? originalName; // 原始文件名
|
||||||
|
final int fileSize; // 文件大小
|
||||||
|
final String mimeType; // MIME类型
|
||||||
|
final int? width; // 图片宽度
|
||||||
|
final int? height; // 图片高度
|
||||||
|
final bool isFavorite; // 是否收藏
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件夹实体
|
||||||
|
class ImageFolder {
|
||||||
|
final String id; // UUID
|
||||||
|
final String name; // 文件夹名称
|
||||||
|
final String? coverImageId; // 封面图片ID
|
||||||
|
final String icon; // Material Icons名称
|
||||||
|
final DateTime createdAt; // 创建时间
|
||||||
|
final DateTime updatedAt; // 更新时间
|
||||||
|
final DateTime lastUsedAt; // 最近使用时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签实体
|
||||||
|
class ImageTag {
|
||||||
|
final String id; // UUID
|
||||||
|
final String name; // 标签名称
|
||||||
|
final String icon; // Material Icons名称
|
||||||
|
final String color; // 十六进制颜色
|
||||||
|
final int usageCount; // 使用次数
|
||||||
|
final DateTime lastUsedAt; // 最近使用时间
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 代码模式
|
## 开发注意事项
|
||||||
|
|
||||||
### 状态管理
|
### 代码规范
|
||||||
- 使用 **StatefulWidget** 进行本地状态管理
|
- 遵循Dart官方代码风格指南
|
||||||
- 使用 **setState** 进行 UI 更新
|
- 使用有意义的变量和函数命名
|
||||||
- 目前使用模拟数据(标有 TODO 注释)
|
- 添加必要的代码注释
|
||||||
|
- 保持widget的纯净性,业务逻辑分离到provider
|
||||||
|
|
||||||
### 导航
|
### 性能要求
|
||||||
- **命名路由** 用于主要导航 (`/add-photo`)
|
- 冷启动时间 < 2秒
|
||||||
- **Navigator.push** 用于照片详情查看
|
- 图片加载流畅,无卡顿
|
||||||
- **BottomNavigationBar** 用于主标签页切换
|
- 用户操作响应 < 200ms
|
||||||
|
- 内存占用 < 200MB
|
||||||
|
|
||||||
### UI 组件
|
### 错误处理
|
||||||
- **Material 3** 设计系统 (`useMaterial3: true`)
|
- 分享失败:"出了点小问题,请重新分享试试~"
|
||||||
- **中文界面**(标签为中文)
|
- 保存失败:同上
|
||||||
- **响应式网格布局** 用于照片显示
|
- 存储空间不足:同上
|
||||||
- **对话框式** 交互用于分类管理
|
- 所有错误都需要友好提示
|
||||||
|
|
||||||
### 数据结构模式
|
### 兼容性要求
|
||||||
- **模拟数据** 目前以 Lists/Maps 实现
|
- Android 7.0+ (API 24+)
|
||||||
- **分类结构**: `{name: String, count: int, isDefault: bool}`
|
- iOS 12.0+
|
||||||
- **标签管理**: 字符串列表支持 # 前缀
|
- 适配各种屏幕尺寸和分辨率
|
||||||
|
|
||||||
## 关键 TODO 项目
|
## 当前开发任务
|
||||||
|
|
||||||
代码库包含多个 TODO 注释,指示计划功能:
|
### 🎯 当前阶段:Phase 1.2 - 数据层架构搭建
|
||||||
- 用真实照片数据替换模拟数据
|
**预计时间:1天**
|
||||||
- 实现从设备选择照片功能
|
**优先级:高**
|
||||||
- 添加照片保存功能
|
|
||||||
- 实现收藏/分享/删除功能
|
|
||||||
- 按分类筛选照片
|
|
||||||
- 集成真实文件夹/分类数据
|
|
||||||
|
|
||||||
## 当前限制
|
**具体任务**:
|
||||||
|
- [ ] 配置Hive数据库和实体模型
|
||||||
|
- [ ] 实现数据访问对象(DAO)模式
|
||||||
|
- [ ] 创建基础Repository接口和实现
|
||||||
|
- [ ] 设置数据迁移和版本管理
|
||||||
|
|
||||||
- 无实际照片存储/检索(仅模拟数据)
|
**已完成任务**:Phase 1.1 - 项目基础配置
|
||||||
- 无相机集成
|
**下一个任务**:Phase 1.3 - 核心工具类
|
||||||
- 无持久化存储
|
|
||||||
- 无图像缓存或优化
|
|
||||||
- 仅基础错误处理
|
|
||||||
|
|
||||||
## 开发环境
|
**开发顺序**:严格按照任务编号顺序执行,每个任务完成后再进行下一个任务。
|
||||||
|
|
||||||
- **Flutter**: 3.2.5+
|
**代码提交**:每个任务完成后提交一次,确保版本控制清晰。
|
||||||
- **Dart**: 3.2.5+
|
|
||||||
- **目标平台**: iOS、Android(支持 Web)
|
---
|
||||||
- **构建工具**: 标准 Flutter 工具链
|
|
||||||
|
**最后更新**:2025年9月12日
|
||||||
|
**开发状态**:Phase 1.2 进行中
|
||||||
|
**下一阶段**:数据层架构搭建...
|
||||||
|
|
||||||
|
**记住:功能优先,保持代码整洁,及时沟通!** 🚀
|
||||||
|
|
||||||
|
## 开发进度跟踪
|
||||||
|
|
||||||
|
### 📊 总体进度
|
||||||
|
- [x] Phase 1.1: 项目基础配置(5/5)✅
|
||||||
|
- [ ] Phase 1.2: 数据层架构搭建(0/4)
|
||||||
|
- [ ] Phase 1.3: 核心工具类(0/4)
|
||||||
|
- [ ] Phase 1.4: 基础UI组件(0/4)
|
||||||
|
- [ ] Phase 2: 分享功能(0/4)
|
||||||
|
|
||||||
|
### 🎯 当前任务详情
|
||||||
|
**任务编号**:1.2
|
||||||
|
**任务名称**:数据层架构搭建
|
||||||
|
**任务状态**:进行中
|
||||||
|
**预计完成**:2025年9月12日
|
||||||
|
**依赖项**:Phase 1.1 完成
|
||||||
|
|
||||||
|
**任务验收标准**:
|
||||||
|
- Hive数据库配置完成
|
||||||
|
- 实体模型定义完整
|
||||||
|
- DAO模式实现正确
|
||||||
|
- Repository接口设计合理
|
||||||
|
- 数据迁移机制可用
|
||||||
36
lib/core/constants/app_constants.dart
Normal file
36
lib/core/constants/app_constants.dart
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/// 应用常量配置
|
||||||
|
class AppConstants {
|
||||||
|
// 应用信息
|
||||||
|
static const String appName = '想拍';
|
||||||
|
static const String appNameEn = 'InspoSnap';
|
||||||
|
static const String appVersion = '1.0.0';
|
||||||
|
static const String appDescription = 'Shoot What Inspires You';
|
||||||
|
|
||||||
|
// 图片处理常量
|
||||||
|
static const int maxThumbnailSize = 500; // 缩略图长边最大尺寸
|
||||||
|
static const int thumbnailQuality = 85; // 缩略图质量
|
||||||
|
static const int maxImageSize = 30 * 1024 * 1024; // 最大图片大小 30MB
|
||||||
|
static const int maxShareImages = 30; // 最大分享图片数量
|
||||||
|
|
||||||
|
// 缓存配置
|
||||||
|
static const Duration cacheMaxAge = Duration(days: 30); // 缓存最大保存时间
|
||||||
|
static const int cacheMaxSize = 500 * 1024 * 1024; // 缓存最大大小 500MB
|
||||||
|
|
||||||
|
// 搜索配置
|
||||||
|
static const int maxSearchHistory = 10; // 搜索历史最大数量
|
||||||
|
static const Duration searchDebounceDuration = Duration(milliseconds: 300); // 搜索防抖时间
|
||||||
|
|
||||||
|
// UI配置
|
||||||
|
static const double defaultPadding = 16.0; // 默认内边距
|
||||||
|
static const double defaultBorderRadius = 8.0; // 默认圆角
|
||||||
|
static const double defaultSpacing = 8.0; // 默认间距
|
||||||
|
static const int mobileMaxWidth = 600; // 手机最大宽度
|
||||||
|
|
||||||
|
// 动画配置
|
||||||
|
static const Duration pageTransitionDuration = Duration(milliseconds: 300); // 页面转场时间
|
||||||
|
static const Duration animationDuration = Duration(milliseconds: 200); // 动画时间
|
||||||
|
|
||||||
|
// 性能配置
|
||||||
|
static const int imageLoadingTimeout = 30; // 图片加载超时时间(秒)
|
||||||
|
static const int maxMemoryCacheCount = 100; // 内存缓存最大图片数量
|
||||||
|
}
|
||||||
22
lib/core/constants/asset_constants.dart
Normal file
22
lib/core/constants/asset_constants.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/// 应用资源文件路径常量
|
||||||
|
class AssetConstants {
|
||||||
|
// 图标资源
|
||||||
|
static const String appIcon = 'assets/icons/app_icon.png';
|
||||||
|
static const String appLogo = 'assets/icons/app_logo.png';
|
||||||
|
|
||||||
|
// 图片资源
|
||||||
|
static const String emptyFolder = 'assets/images/empty_folder.png';
|
||||||
|
static const String emptyGallery = 'assets/images/empty_gallery.png';
|
||||||
|
static const String emptySearch = 'assets/images/empty_search.png';
|
||||||
|
static const String errorState = 'assets/images/error_state.png';
|
||||||
|
static const String loadingPlaceholder = 'assets/images/loading_placeholder.png';
|
||||||
|
|
||||||
|
// 动画资源
|
||||||
|
static const String loadingAnimation = 'assets/animations/loading.json';
|
||||||
|
static const String successAnimation = 'assets/animations/success.json';
|
||||||
|
static const String errorAnimation = 'assets/animations/error.json';
|
||||||
|
|
||||||
|
// 字体资源(如果需要自定义字体)
|
||||||
|
static const String primaryFont = 'NotoSans';
|
||||||
|
static const String secondaryFont = 'Roboto';
|
||||||
|
}
|
||||||
25
lib/core/constants/hive_constants.dart
Normal file
25
lib/core/constants/hive_constants.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/// Hive数据库常量配置
|
||||||
|
class HiveConstants {
|
||||||
|
// 数据库名称
|
||||||
|
static const String databaseName = 'insposnap_db';
|
||||||
|
|
||||||
|
// Box名称
|
||||||
|
static const String imagesBox = 'images';
|
||||||
|
static const String foldersBox = 'folders';
|
||||||
|
static const String tagsBox = 'tags';
|
||||||
|
static const String settingsBox = 'settings';
|
||||||
|
static const String searchHistoryBox = 'search_history';
|
||||||
|
|
||||||
|
// 设置相关Key
|
||||||
|
static const String themeModeKey = 'theme_mode';
|
||||||
|
static const String localeKey = 'locale';
|
||||||
|
static const String gridLayoutKey = 'grid_layout';
|
||||||
|
static const String firstLaunchKey = 'first_launch';
|
||||||
|
|
||||||
|
// 数据库版本
|
||||||
|
static const int databaseVersion = 1;
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
static const int defaultPageSize = 20; // 默认分页大小
|
||||||
|
static const int maxPageSize = 100; // 最大分页大小
|
||||||
|
}
|
||||||
26
lib/core/constants/route_constants.dart
Normal file
26
lib/core/constants/route_constants.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/// 应用路由名称常量
|
||||||
|
class RouteConstants {
|
||||||
|
// 主页面路由
|
||||||
|
static const String main = '/';
|
||||||
|
static const String gallery = '/gallery';
|
||||||
|
static const String folders = '/folders';
|
||||||
|
static const String tags = '/tags';
|
||||||
|
static const String settings = '/settings';
|
||||||
|
|
||||||
|
// 功能页面路由
|
||||||
|
static const String saveImage = '/save-image';
|
||||||
|
static const String imageDetail = '/image-detail';
|
||||||
|
static const String folderDetail = '/folder-detail';
|
||||||
|
static const String tagDetail = '/tag-detail';
|
||||||
|
|
||||||
|
// 编辑页面路由
|
||||||
|
static const String editImage = '/edit-image';
|
||||||
|
static const String editFolder = '/edit-folder';
|
||||||
|
static const String editTag = '/edit-tag';
|
||||||
|
|
||||||
|
// 设置相关路由
|
||||||
|
static const String languageSettings = '/settings/language';
|
||||||
|
static const String themeSettings = '/settings/theme';
|
||||||
|
static const String storageSettings = '/settings/storage';
|
||||||
|
static const String aboutSettings = '/settings/about';
|
||||||
|
}
|
||||||
90
lib/core/errors/app_error.dart
Normal file
90
lib/core/errors/app_error.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/// 应用错误基类
|
||||||
|
/// 所有自定义错误都应该继承此类
|
||||||
|
abstract class AppError implements Exception {
|
||||||
|
final String message;
|
||||||
|
final String? code;
|
||||||
|
final StackTrace? stackTrace;
|
||||||
|
|
||||||
|
AppError({
|
||||||
|
required this.message,
|
||||||
|
this.code,
|
||||||
|
this.stackTrace,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AppError: $message${code != null ? ' (Code: $code)' : ''}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 图片处理相关错误
|
||||||
|
class ImageProcessingError extends AppError {
|
||||||
|
ImageProcessingError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 文件存储相关错误
|
||||||
|
class StorageError extends AppError {
|
||||||
|
StorageError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库相关错误
|
||||||
|
class DatabaseError extends AppError {
|
||||||
|
DatabaseError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分享接收相关错误
|
||||||
|
class ShareReceiveError extends AppError {
|
||||||
|
ShareReceiveError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 网络相关错误
|
||||||
|
class NetworkError extends AppError {
|
||||||
|
NetworkError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 权限相关错误
|
||||||
|
class PermissionError extends AppError {
|
||||||
|
PermissionError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证错误
|
||||||
|
class ValidationError extends AppError {
|
||||||
|
ValidationError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 业务逻辑错误
|
||||||
|
class BusinessError extends AppError {
|
||||||
|
BusinessError({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
super.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
235
lib/core/errors/error_handler.dart
Normal file
235
lib/core/errors/error_handler.dart
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'app_error.dart';
|
||||||
|
|
||||||
|
/// 错误处理工具类
|
||||||
|
/// 提供统一的错误处理、日志记录和用户友好的错误消息
|
||||||
|
class ErrorHandler {
|
||||||
|
// 私有构造函数,防止实例化
|
||||||
|
ErrorHandler._();
|
||||||
|
|
||||||
|
/// 记录错误日志
|
||||||
|
static void logError(
|
||||||
|
dynamic error, {
|
||||||
|
String? message,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
String? source,
|
||||||
|
}) {
|
||||||
|
final logMessage = StringBuffer();
|
||||||
|
|
||||||
|
// 添加错误来源
|
||||||
|
if (source != null) {
|
||||||
|
logMessage.write('[$source] ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加自定义消息
|
||||||
|
if (message != null) {
|
||||||
|
logMessage.write('$message: ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加错误信息
|
||||||
|
if (error is AppError) {
|
||||||
|
logMessage.write('${error.message} (${error.code ?? 'UNKNOWN'})');
|
||||||
|
} else if (error is Exception) {
|
||||||
|
logMessage.write(error.toString());
|
||||||
|
} else {
|
||||||
|
logMessage.write(error?.toString() ?? 'Unknown error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发模式下打印详细错误信息
|
||||||
|
if (kDebugMode) {
|
||||||
|
developer.log(
|
||||||
|
logMessage.toString(),
|
||||||
|
name: 'InspoSnap',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 打印堆栈跟踪
|
||||||
|
if (stackTrace != null) {
|
||||||
|
developer.log('StackTrace: $stackTrace', name: 'InspoSnap');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户友好的错误消息
|
||||||
|
static String getUserFriendlyMessage(dynamic error) {
|
||||||
|
if (error is AppError) {
|
||||||
|
return _getAppErrorMessage(error);
|
||||||
|
} else if (error is Exception) {
|
||||||
|
return _getExceptionMessage(error);
|
||||||
|
} else {
|
||||||
|
return '出了点小问题,请稍后重试';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用错误的用户友好消息
|
||||||
|
static String _getAppErrorMessage(AppError error) {
|
||||||
|
if (error is ImageProcessingError) {
|
||||||
|
return '图片处理失败,请检查图片格式或重试';
|
||||||
|
} else if (error is StorageError) {
|
||||||
|
return '存储空间不足,请清理空间后重试';
|
||||||
|
} else if (error is DatabaseError) {
|
||||||
|
return '数据保存失败,请重试';
|
||||||
|
} else if (error is ShareReceiveError) {
|
||||||
|
return '出了点小问题,请重新分享试试~';
|
||||||
|
} else if (error is NetworkError) {
|
||||||
|
return '网络连接异常,请检查网络后重试';
|
||||||
|
} else if (error is PermissionError) {
|
||||||
|
return '需要相关权限才能继续使用';
|
||||||
|
} else if (error is ValidationError) {
|
||||||
|
return error.message; // 验证错误直接显示具体消息
|
||||||
|
} else if (error is BusinessError) {
|
||||||
|
return error.message; // 业务错误直接显示具体消息
|
||||||
|
} else {
|
||||||
|
return error.message; // 其他应用错误直接显示消息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取异常的用户友好消息
|
||||||
|
static String _getExceptionMessage(Exception exception) {
|
||||||
|
final message = exception.toString();
|
||||||
|
|
||||||
|
// 根据异常类型返回相应的用户友好消息
|
||||||
|
if (message.contains('OutOfMemoryError')) {
|
||||||
|
return '内存不足,请关闭其他应用后重试';
|
||||||
|
} else if (message.contains('FileSystemException')) {
|
||||||
|
return '文件操作失败,请检查存储权限';
|
||||||
|
} else if (message.contains('SocketException')) {
|
||||||
|
return '网络连接失败,请检查网络设置';
|
||||||
|
} else if (message.contains('TimeoutException')) {
|
||||||
|
return '操作超时,请稍后重试';
|
||||||
|
} else {
|
||||||
|
return '出了点小问题,请稍后重试';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 包装异步函数,统一处理错误
|
||||||
|
static Future<T> wrapAsync<T>(
|
||||||
|
Future<T> Function() asyncFunction, {
|
||||||
|
String? source,
|
||||||
|
T? fallbackValue,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await asyncFunction();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logError(error, source: source, stackTrace: stackTrace);
|
||||||
|
|
||||||
|
if (fallbackValue != null) {
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是AppError,重新抛出
|
||||||
|
if (error is AppError) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将其他错误包装为StorageError
|
||||||
|
throw StorageError(
|
||||||
|
message: getUserFriendlyMessage(error),
|
||||||
|
code: ErrorCodes.wrappedError,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 包装同步函数,统一处理错误
|
||||||
|
static T wrapSync<T>(
|
||||||
|
T Function() syncFunction, {
|
||||||
|
String? source,
|
||||||
|
T? fallbackValue,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
return syncFunction();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logError(error, source: source, stackTrace: stackTrace);
|
||||||
|
|
||||||
|
if (fallbackValue != null) {
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是AppError,重新抛出
|
||||||
|
if (error is AppError) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将其他错误包装为StorageError
|
||||||
|
throw StorageError(
|
||||||
|
message: getUserFriendlyMessage(error),
|
||||||
|
code: ErrorCodes.wrappedError,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否为致命错误
|
||||||
|
static bool isFatalError(dynamic error) {
|
||||||
|
if (error is AppError) {
|
||||||
|
return error.code == 'DATABASE_ERROR' ||
|
||||||
|
error.code == 'STORAGE_ERROR' ||
|
||||||
|
error.code == 'PERMISSION_ERROR';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取错误代码
|
||||||
|
static String? getErrorCode(dynamic error) {
|
||||||
|
if (error is AppError) {
|
||||||
|
return error.code;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取错误堆栈
|
||||||
|
static StackTrace? getStackTrace(dynamic error) {
|
||||||
|
if (error is AppError) {
|
||||||
|
return error.stackTrace;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 错误消息常量
|
||||||
|
class ErrorMessages {
|
||||||
|
static const String unknownError = '出了点小问题,请稍后重试';
|
||||||
|
static const String networkError = '网络连接异常,请检查网络后重试';
|
||||||
|
static const String serverError = '服务器繁忙,请稍后重试';
|
||||||
|
static const String timeoutError = '请求超时,请稍后重试';
|
||||||
|
static const String permissionError = '需要相关权限才能继续使用';
|
||||||
|
static const String storageError = '存储空间不足,请清理空间后重试';
|
||||||
|
static const String invalidInput = '输入内容格式不正确,请检查后重试';
|
||||||
|
static const String dataNotFound = '未找到相关数据';
|
||||||
|
static const String operationFailed = '操作失败,请重试';
|
||||||
|
static const String imageProcessingError = '图片处理失败,请检查图片格式或重试';
|
||||||
|
static const String shareReceiveError = '出了点小问题,请重新分享试试~';
|
||||||
|
static const String databaseError = '数据保存失败,请重试';
|
||||||
|
static const String validationError = '数据验证失败,请检查后重试';
|
||||||
|
static const String businessError = '业务处理失败,请稍后重试';
|
||||||
|
static const String memoryError = '内存不足,请关闭其他应用后重试';
|
||||||
|
static const String fileSystemError = '文件操作失败,请检查存储权限';
|
||||||
|
static const String socketError = '网络连接失败,请检查网络设置';
|
||||||
|
static const String timeoutException = '操作超时,请稍后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 错误代码常量
|
||||||
|
class ErrorCodes {
|
||||||
|
static const String unknownError = 'UNKNOWN_ERROR';
|
||||||
|
static const String networkError = 'NETWORK_ERROR';
|
||||||
|
static const String serverError = 'SERVER_ERROR';
|
||||||
|
static const String timeoutError = 'TIMEOUT_ERROR';
|
||||||
|
static const String permissionError = 'PERMISSION_ERROR';
|
||||||
|
static const String storageError = 'STORAGE_ERROR';
|
||||||
|
static const String invalidInput = 'INVALID_INPUT';
|
||||||
|
static const String dataNotFound = 'DATA_NOT_FOUND';
|
||||||
|
static const String operationFailed = 'OPERATION_FAILED';
|
||||||
|
static const String imageProcessingError = 'IMAGE_PROCESSING_ERROR';
|
||||||
|
static const String shareReceiveError = 'SHARE_RECEIVE_ERROR';
|
||||||
|
static const String databaseError = 'DATABASE_ERROR';
|
||||||
|
static const String validationError = 'VALIDATION_ERROR';
|
||||||
|
static const String businessError = 'BUSINESS_ERROR';
|
||||||
|
static const String memoryError = 'MEMORY_ERROR';
|
||||||
|
static const String fileSystemError = 'FILE_SYSTEM_ERROR';
|
||||||
|
static const String socketError = 'SOCKET_ERROR';
|
||||||
|
static const String timeoutException = 'TIMEOUT_EXCEPTION';
|
||||||
|
static const String wrappedError = 'WRAPPED_ERROR';
|
||||||
|
}
|
||||||
139
lib/core/theme/app_colors.dart
Normal file
139
lib/core/theme/app_colors.dart
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 应用颜色配置类
|
||||||
|
/// 提供一致的颜色管理和主题色彩支持
|
||||||
|
class AppColors {
|
||||||
|
// 私有构造函数,防止实例化
|
||||||
|
AppColors._();
|
||||||
|
|
||||||
|
// 主要品牌色彩
|
||||||
|
static const MaterialColor primarySwatch = MaterialColor(
|
||||||
|
0xFF2196F3,
|
||||||
|
<int, Color>{
|
||||||
|
50: Color(0xFFE3F2FD),
|
||||||
|
100: Color(0xFFBBDEFB),
|
||||||
|
200: Color(0xFF90CAF9),
|
||||||
|
300: Color(0xFF64B5F6),
|
||||||
|
400: Color(0xFF42A5F5),
|
||||||
|
500: Color(0xFF2196F3),
|
||||||
|
600: Color(0xFF1E88E5),
|
||||||
|
700: Color(0xFF1976D2),
|
||||||
|
800: Color(0xFF1565C0),
|
||||||
|
900: Color(0xFF0D47A1),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 基础色彩
|
||||||
|
static const Color primary = Color(0xFF2196F3);
|
||||||
|
static const Color primaryLight = Color(0xFF64B5F6);
|
||||||
|
static const Color primaryDark = Color(0xFF1976D2);
|
||||||
|
|
||||||
|
static const Color secondary = Color(0xFF03DAC6);
|
||||||
|
static const Color secondaryLight = Color(0xFF5DF2E3);
|
||||||
|
static const Color secondaryDark = Color(0xFF00BFA5);
|
||||||
|
|
||||||
|
// 状态色彩
|
||||||
|
static const Color success = Color(0xFF4CAF50);
|
||||||
|
static const Color warning = Color(0xFFFFC107);
|
||||||
|
static const Color error = Color(0xFFF44336);
|
||||||
|
static const Color info = Color(0xFF2196F3);
|
||||||
|
|
||||||
|
// 中性色彩(浅色模式)
|
||||||
|
static const Color backgroundLight = Color(0xFFF5F5F5);
|
||||||
|
static const Color surfaceLight = Colors.white;
|
||||||
|
static const Color textPrimaryLight = Color(0xFF212121);
|
||||||
|
static const Color textSecondaryLight = Color(0xFF757575);
|
||||||
|
static const Color textHintLight = Color(0xFFBDBDBD);
|
||||||
|
static const Color dividerLight = Color(0xFFE0E0E0);
|
||||||
|
static const Color borderLight = Color(0xFFCCCCCC);
|
||||||
|
|
||||||
|
// 中性色彩(深色模式)- 保持一定的对比度
|
||||||
|
static const Color backgroundDark = Color(0xFF121212);
|
||||||
|
static const Color surfaceDark = Color(0xFF1E1E1E);
|
||||||
|
static const Color surfaceVariantDark = Color(0xFF2C2C2C);
|
||||||
|
static const Color textPrimaryDark = Colors.white;
|
||||||
|
static const Color textSecondaryDark = Color(0xFFB0B0B0);
|
||||||
|
static const Color textHintDark = Color(0xFF808080);
|
||||||
|
static const Color dividerDark = Color(0xFF3C3C3C);
|
||||||
|
static const Color borderDark = Color(0xFF4C4C4C);
|
||||||
|
|
||||||
|
// 功能色彩
|
||||||
|
static const Color favorite = Color(0xFFFF5252);
|
||||||
|
static const Color bookmark = Color(0xFF448AFF);
|
||||||
|
static const Color share = Color(0xFF8BC34A);
|
||||||
|
static const Color download = Color(0xFF00BCD4);
|
||||||
|
|
||||||
|
// 标签色彩(用于标签的颜色选择)
|
||||||
|
static const List<Color> tagColors = [
|
||||||
|
Color(0xFFF44336), // 红色
|
||||||
|
Color(0xFFE91E63), // 粉色
|
||||||
|
Color(0xFF9C27B0), // 紫色
|
||||||
|
Color(0xFF673AB7), // 深紫色
|
||||||
|
Color(0xFF3F51B5), // 靛蓝色
|
||||||
|
Color(0xFF2196F3), // 蓝色
|
||||||
|
Color(0xFF03A9F4), // 亮蓝色
|
||||||
|
Color(0xFF00BCD4), // 青色
|
||||||
|
Color(0xFF009688), // 蓝绿色
|
||||||
|
Color(0xFF4CAF50), // 绿色
|
||||||
|
Color(0xFF8BC34A), // 浅绿色
|
||||||
|
Color(0xFFCDDC39), // 黄绿色
|
||||||
|
Color(0xFFFFEB3B), // 黄色
|
||||||
|
Color(0xFFFFC107), // 琥珀色
|
||||||
|
Color(0xFFFF9800), // 橙色
|
||||||
|
Color(0xFFFF5722), // 深橙色
|
||||||
|
Color(0xFF795548), // 棕色
|
||||||
|
Color(0xFF9E9E9E), // 灰色
|
||||||
|
Color(0xFF607D8B), // 蓝灰色
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 根据背景色获取对比色(确保文字可读性)
|
||||||
|
static Color getContrastColor(Color backgroundColor) {
|
||||||
|
// 计算颜色的亮度(0-1)
|
||||||
|
final brightness = backgroundColor.computeLuminance();
|
||||||
|
// 亮度大于0.5使用深色,否则使用浅色
|
||||||
|
return brightness > 0.5 ? Colors.black : Colors.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从标签色彩列表中随机获取一个颜色
|
||||||
|
static Color getRandomTagColor(int index) {
|
||||||
|
return tagColors[index % tagColors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据十六进制字符串获取颜色
|
||||||
|
static Color fromHex(String hexString) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
|
||||||
|
buffer.write(hexString.replaceFirst('#', ''));
|
||||||
|
return Color(int.parse(buffer.toString(), radix: 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将颜色转换为十六进制字符串
|
||||||
|
static String toHex(Color color) {
|
||||||
|
return '#${color.value.toRadixString(16).padLeft(8, '0').substring(2)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取主题对应的颜色
|
||||||
|
static Color getBackgroundColor(bool isDark) {
|
||||||
|
return isDark ? backgroundDark : backgroundLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getSurfaceColor(bool isDark) {
|
||||||
|
return isDark ? surfaceDark : surfaceLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getTextPrimaryColor(bool isDark) {
|
||||||
|
return isDark ? textPrimaryDark : textPrimaryLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getTextSecondaryColor(bool isDark) {
|
||||||
|
return isDark ? textSecondaryDark : textSecondaryLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getDividerColor(bool isDark) {
|
||||||
|
return isDark ? dividerDark : dividerLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getBorderColor(bool isDark) {
|
||||||
|
return isDark ? borderDark : borderLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
246
lib/core/theme/app_theme.dart
Normal file
246
lib/core/theme/app_theme.dart
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// 应用主题配置类
|
||||||
|
class AppTheme {
|
||||||
|
// 私有构造函数,防止实例化
|
||||||
|
AppTheme._();
|
||||||
|
|
||||||
|
/// 主题色彩配置
|
||||||
|
static const Color primaryColor = Color(0xFF2196F3);
|
||||||
|
static const Color secondaryColor = Color(0xFF03DAC6);
|
||||||
|
static const Color backgroundColor = Color(0xFFF5F5F5);
|
||||||
|
static const Color surfaceColor = Colors.white;
|
||||||
|
static const Color errorColor = Color(0xFFB00020);
|
||||||
|
|
||||||
|
/// 文字颜色
|
||||||
|
static const Color textPrimary = Color(0xFF212121);
|
||||||
|
static const Color textSecondary = Color(0xFF757575);
|
||||||
|
static const Color textHint = Color(0xFFBDBDBD);
|
||||||
|
|
||||||
|
/// 边框和分割线颜色
|
||||||
|
static const Color dividerColor = Color(0xFFE0E0E0);
|
||||||
|
static const Color borderColor = Color(0xFFCCCCCC);
|
||||||
|
|
||||||
|
/// 浅色主题
|
||||||
|
static ThemeData lightTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
colorScheme: const ColorScheme.light(
|
||||||
|
primary: primaryColor,
|
||||||
|
secondary: secondaryColor,
|
||||||
|
surface: surfaceColor,
|
||||||
|
background: backgroundColor,
|
||||||
|
error: errorColor,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.black,
|
||||||
|
onSurface: textPrimary,
|
||||||
|
onBackground: textPrimary,
|
||||||
|
onError: Colors.white,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 应用栏主题
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: surfaceColor,
|
||||||
|
foregroundColor: textPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.dark,
|
||||||
|
statusBarBrightness: Brightness.light,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 卡片主题
|
||||||
|
cardTheme: CardTheme(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.all(8.0),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 按钮主题
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 输入框主题
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey[100],
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: const BorderSide(color: borderColor),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: const BorderSide(color: borderColor),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: const BorderSide(color: primaryColor, width: 2.0),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 文字主题
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
displayMedium: TextStyle(fontSize: 45.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
displaySmall: TextStyle(fontSize: 36.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
headlineLarge: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
headlineMedium: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
headlineSmall: TextStyle(fontSize: 24.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
titleMedium: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500, color: textPrimary),
|
||||||
|
titleSmall: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: textPrimary),
|
||||||
|
bodyLarge: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
bodyMedium: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400, color: textPrimary),
|
||||||
|
bodySmall: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w400, color: textSecondary),
|
||||||
|
labelLarge: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: textPrimary),
|
||||||
|
labelMedium: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w500, color: textPrimary),
|
||||||
|
labelSmall: TextStyle(fontSize: 11.0, fontWeight: FontWeight.w500, color: textPrimary),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 图标主题
|
||||||
|
iconTheme: const IconThemeData(
|
||||||
|
color: textSecondary,
|
||||||
|
size: 24.0,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 分割线主题
|
||||||
|
dividerTheme: const DividerThemeData(
|
||||||
|
color: dividerColor,
|
||||||
|
thickness: 1.0,
|
||||||
|
space: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 深色主题
|
||||||
|
static ThemeData darkTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
colorScheme: const ColorScheme.dark(
|
||||||
|
primary: primaryColor,
|
||||||
|
secondary: secondaryColor,
|
||||||
|
surface: Color(0xFF121212),
|
||||||
|
background: Color(0xFF121212),
|
||||||
|
error: errorColor,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.black,
|
||||||
|
onSurface: Colors.white,
|
||||||
|
onBackground: Colors.white,
|
||||||
|
onError: Colors.white,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 应用栏主题(深色模式保持一定对比度)
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Color(0xFF1E1E1E),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
statusBarBrightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 卡片主题
|
||||||
|
cardTheme: CardTheme(
|
||||||
|
elevation: 4,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.all(8.0),
|
||||||
|
color: const Color(0xFF1E1E1E),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 按钮主题
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 输入框主题
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFF2C2C2C),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF3C3C3C)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF3C3C3C)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: const BorderSide(color: primaryColor, width: 2.0),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 文字主题(深色模式保持可读性)
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
displayMedium: TextStyle(fontSize: 45.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
displaySmall: TextStyle(fontSize: 36.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
headlineLarge: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
headlineMedium: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
headlineSmall: TextStyle(fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
titleMedium: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500, color: Colors.white),
|
||||||
|
titleSmall: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white),
|
||||||
|
bodyLarge: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
bodyMedium: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.white),
|
||||||
|
bodySmall: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w400, color: Color(0xFFB0B0B0)),
|
||||||
|
labelLarge: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white),
|
||||||
|
labelMedium: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w500, color: Colors.white),
|
||||||
|
labelSmall: TextStyle(fontSize: 11.0, fontWeight: FontWeight.w500, color: Colors.white),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 图标主题
|
||||||
|
iconTheme: const IconThemeData(
|
||||||
|
color: Color(0xFFB0B0B0),
|
||||||
|
size: 24.0,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 分割线主题
|
||||||
|
dividerTheme: const DividerThemeData(
|
||||||
|
color: Color(0xFF3C3C3C),
|
||||||
|
thickness: 1.0,
|
||||||
|
space: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 获取主题模式对应的主题数据
|
||||||
|
static ThemeData getTheme(Brightness brightness) {
|
||||||
|
return brightness == Brightness.dark ? darkTheme : lightTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前主题的对比色(用于保证可读性)
|
||||||
|
static Color getContrastColor(Color backgroundColor) {
|
||||||
|
// 计算颜色的亮度
|
||||||
|
final brightness = backgroundColor.computeLuminance();
|
||||||
|
// 亮度大于0.5使用深色,否则使用浅色
|
||||||
|
return brightness > 0.5 ? Colors.black : Colors.white;
|
||||||
|
}
|
||||||
|
}
|
||||||
454
lib/core/utils/file_utils.dart
Normal file
454
lib/core/utils/file_utils.dart
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import '../errors/app_error.dart';
|
||||||
|
import 'logger.dart';
|
||||||
|
|
||||||
|
/// 文件工具类
|
||||||
|
/// 提供文件存储、路径管理、存储空间检查等功能
|
||||||
|
class FileUtils {
|
||||||
|
// 私有构造函数,防止实例化
|
||||||
|
FileUtils._();
|
||||||
|
|
||||||
|
/// 获取应用文档目录
|
||||||
|
static Future<Directory> getApplicationDocumentsDirectory() async {
|
||||||
|
try {
|
||||||
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
|
Logger.debug('获取应用文档目录: ${directory.path}');
|
||||||
|
return directory;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('获取应用文档目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '获取应用文档目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取临时目录
|
||||||
|
static Future<Directory> getTemporaryDirectory() async {
|
||||||
|
try {
|
||||||
|
final directory = await getTemporaryDirectory();
|
||||||
|
Logger.debug('获取临时目录: ${directory.path}');
|
||||||
|
return directory;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('获取临时目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '获取临时目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用支持目录(用于存储应用数据)
|
||||||
|
static Future<Directory> getApplicationSupportDirectory() async {
|
||||||
|
try {
|
||||||
|
final directory = await getApplicationSupportDirectory();
|
||||||
|
Logger.debug('获取应用支持目录: ${directory.path}');
|
||||||
|
return directory;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('获取应用支持目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '获取应用支持目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建应用存储目录结构
|
||||||
|
/// 按照日期格式创建目录:/yyyy/MM/dd/
|
||||||
|
static Future<String> createStorageDirectory(DateTime date) async {
|
||||||
|
try {
|
||||||
|
final supportDir = await getApplicationSupportDirectory();
|
||||||
|
|
||||||
|
// 构建日期路径
|
||||||
|
final year = date.year.toString();
|
||||||
|
final month = date.month.toString().padLeft(2, '0');
|
||||||
|
final day = date.day.toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
final storagePath = path.join(supportDir.path, 'images', year, month, day);
|
||||||
|
final storageDir = Directory(storagePath);
|
||||||
|
|
||||||
|
// 如果目录不存在,创建目录
|
||||||
|
if (!await storageDir.exists()) {
|
||||||
|
await storageDir.create(recursive: true);
|
||||||
|
Logger.debug('创建存储目录: $storagePath');
|
||||||
|
}
|
||||||
|
|
||||||
|
return storagePath;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('创建存储目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '创建存储目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建缩略图存储目录
|
||||||
|
static Future<String> createThumbnailDirectory(DateTime date) async {
|
||||||
|
try {
|
||||||
|
final supportDir = await getApplicationSupportDirectory();
|
||||||
|
|
||||||
|
// 构建日期路径
|
||||||
|
final year = date.year.toString();
|
||||||
|
final month = date.month.toString().padLeft(2, '0');
|
||||||
|
final day = date.day.toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
final thumbnailPath = path.join(supportDir.path, 'thumbnails', year, month, day);
|
||||||
|
final thumbnailDir = Directory(thumbnailPath);
|
||||||
|
|
||||||
|
// 如果目录不存在,创建目录
|
||||||
|
if (!await thumbnailDir.exists()) {
|
||||||
|
await thumbnailDir.create(recursive: true);
|
||||||
|
Logger.debug('创建缩略图目录: $thumbnailPath');
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnailPath;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('创建缩略图目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '创建缩略图目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建临时文件目录
|
||||||
|
static Future<String> createTempDirectory() async {
|
||||||
|
try {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final inspoSnapTempPath = path.join(tempDir.path, 'insposnap_temp');
|
||||||
|
final tempDirectory = Directory(inspoSnapTempPath);
|
||||||
|
|
||||||
|
if (!await tempDirectory.exists()) {
|
||||||
|
await tempDirectory.create(recursive: true);
|
||||||
|
Logger.debug('创建临时目录: $inspoSnapTempPath');
|
||||||
|
}
|
||||||
|
|
||||||
|
return inspoSnapTempPath;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('创建临时目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '创建临时目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查存储空间是否充足
|
||||||
|
static Future<bool> hasEnoughStorageSpace({int requiredBytes = 100 * 1024 * 1024}) async {
|
||||||
|
try {
|
||||||
|
final directory = await getApplicationSupportDirectory();
|
||||||
|
// final stat = await FileStat.stat(directory.path);
|
||||||
|
|
||||||
|
// 获取可用空间(这里简化处理,实际需要更复杂的计算)
|
||||||
|
// 在实际应用中,可能需要使用 platform-specific 代码来获取准确的可用空间
|
||||||
|
|
||||||
|
// 模拟检查:如果目录存在且有写入权限,则认为空间充足
|
||||||
|
final testFile = File(path.join(directory.path, '.space_test'));
|
||||||
|
try {
|
||||||
|
await testFile.writeAsString('test');
|
||||||
|
await testFile.delete();
|
||||||
|
Logger.debug('存储空间检查通过');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
Logger.warning('存储空间可能不足或没有写入权限');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('检查存储空间失败', error: error, stackTrace: stackTrace);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取目录大小(字节)
|
||||||
|
static Future<int> getDirectorySize(Directory directory) async {
|
||||||
|
try {
|
||||||
|
int totalSize = 0;
|
||||||
|
|
||||||
|
if (!await directory.exists()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await for (final entity in directory.list(recursive: true, followLinks: false)) {
|
||||||
|
if (entity is File) {
|
||||||
|
try {
|
||||||
|
final stat = await entity.stat();
|
||||||
|
totalSize += stat.size;
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略单个文件的错误,继续统计其他文件
|
||||||
|
Logger.warning('获取文件大小失败: ${entity.path}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug('目录大小: ${directory.path} = ${(totalSize / 1024 / 1024).toStringAsFixed(2)}MB');
|
||||||
|
return totalSize;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('获取目录大小失败', error: error, stackTrace: stackTrace);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除目录及其所有内容
|
||||||
|
static Future<void> deleteDirectory(Directory directory) async {
|
||||||
|
try {
|
||||||
|
if (await directory.exists()) {
|
||||||
|
await directory.delete(recursive: true);
|
||||||
|
Logger.debug('删除目录: ${directory.path}');
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('删除目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '删除目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清空目录内容(保留目录本身)
|
||||||
|
static Future<void> clearDirectory(Directory directory) async {
|
||||||
|
try {
|
||||||
|
if (!await directory.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await for (final entity in directory.list(followLinks: false)) {
|
||||||
|
try {
|
||||||
|
if (entity is File) {
|
||||||
|
await entity.delete();
|
||||||
|
} else if (entity is Directory) {
|
||||||
|
await entity.delete(recursive: true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略单个文件/目录的删除错误
|
||||||
|
Logger.warning('删除失败: ${entity.path}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug('清空目录: ${directory.path}');
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('清空目录失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '清空目录失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 复制文件
|
||||||
|
static Future<File> copyFile(String sourcePath, String targetPath) async {
|
||||||
|
try {
|
||||||
|
final sourceFile = File(sourcePath);
|
||||||
|
if (!await sourceFile.exists()) {
|
||||||
|
throw StorageError(
|
||||||
|
message: '源文件不存在: $sourcePath',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
final targetDir = Directory(path.dirname(targetPath));
|
||||||
|
if (!await targetDir.exists()) {
|
||||||
|
await targetDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetFile = await sourceFile.copy(targetPath);
|
||||||
|
Logger.debug('复制文件: $sourcePath -> $targetPath');
|
||||||
|
return targetFile;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('复制文件失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '复制文件失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 移动文件
|
||||||
|
static Future<File> moveFile(String sourcePath, String targetPath) async {
|
||||||
|
try {
|
||||||
|
final sourceFile = File(sourcePath);
|
||||||
|
if (!await sourceFile.exists()) {
|
||||||
|
throw StorageError(
|
||||||
|
message: '源文件不存在: $sourcePath',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
final targetDir = Directory(path.dirname(targetPath));
|
||||||
|
if (!await targetDir.exists()) {
|
||||||
|
await targetDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetFile = await sourceFile.rename(targetPath);
|
||||||
|
Logger.debug('移动文件: $sourcePath -> $targetPath');
|
||||||
|
return targetFile;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('移动文件失败', error: error, stackTrace: stackTrace);
|
||||||
|
throw StorageError(
|
||||||
|
message: '移动文件失败: ${error.toString()}',
|
||||||
|
code: 'STORAGE_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取文件扩展名
|
||||||
|
static String getFileExtension(String filePath) {
|
||||||
|
return path.extension(filePath).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取文件名(不含路径)
|
||||||
|
static String getFileName(String filePath) {
|
||||||
|
return path.basename(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取文件名(不含扩展名)
|
||||||
|
static String getFileNameWithoutExtension(String filePath) {
|
||||||
|
return path.basenameWithoutExtension(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成唯一的文件名
|
||||||
|
static String generateUniqueFileName(String originalFileName) {
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final extension = getFileExtension(originalFileName);
|
||||||
|
final nameWithoutExt = getFileNameWithoutExtension(originalFileName);
|
||||||
|
return '${nameWithoutExt}_$timestamp$extension';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查文件是否存在
|
||||||
|
static Future<bool> fileExists(String filePath) async {
|
||||||
|
try {
|
||||||
|
final file = File(filePath);
|
||||||
|
return await file.exists();
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('检查文件存在失败', error: error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取文件大小(字节)
|
||||||
|
static Future<int> getFileSize(String filePath) async {
|
||||||
|
try {
|
||||||
|
final file = File(filePath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
final stat = await file.stat();
|
||||||
|
return stat.size;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('获取文件大小失败', error: error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 格式化文件大小
|
||||||
|
static String formatFileSize(int bytes) {
|
||||||
|
if (bytes <= 0) return '0 B';
|
||||||
|
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
int unitIndex = 0;
|
||||||
|
double size = bytes.toDouble();
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${size.toStringAsFixed(2)} ${units[unitIndex]}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清理临时文件
|
||||||
|
static Future<void> cleanTempFiles() async {
|
||||||
|
try {
|
||||||
|
final tempDir = Directory(await createTempDirectory());
|
||||||
|
await clearDirectory(tempDir);
|
||||||
|
Logger.info('临时文件清理完成');
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('清理临时文件失败', error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取存储使用情况
|
||||||
|
static Future<Map<String, dynamic>> getStorageUsage() async {
|
||||||
|
try {
|
||||||
|
final supportDir = await getApplicationSupportDirectory();
|
||||||
|
final imagesDir = Directory(path.join(supportDir.path, 'images'));
|
||||||
|
final thumbnailsDir = Directory(path.join(supportDir.path, 'thumbnails'));
|
||||||
|
|
||||||
|
final imagesSize = await getDirectorySize(imagesDir);
|
||||||
|
final thumbnailsSize = await getDirectorySize(thumbnailsDir);
|
||||||
|
final totalSize = imagesSize + thumbnailsSize;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'imagesSize': imagesSize,
|
||||||
|
'thumbnailsSize': thumbnailsSize,
|
||||||
|
'totalSize': totalSize,
|
||||||
|
'imagesSizeFormatted': formatFileSize(imagesSize),
|
||||||
|
'thumbnailsSizeFormatted': formatFileSize(thumbnailsSize),
|
||||||
|
'totalSizeFormatted': formatFileSize(totalSize),
|
||||||
|
};
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('获取存储使用情况失败', error: error, stackTrace: stackTrace);
|
||||||
|
return {
|
||||||
|
'imagesSize': 0,
|
||||||
|
'thumbnailsSize': 0,
|
||||||
|
'totalSize': 0,
|
||||||
|
'imagesSizeFormatted': '0 B',
|
||||||
|
'thumbnailsSizeFormatted': '0 B',
|
||||||
|
'totalSizeFormatted': '0 B',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 文件信息类
|
||||||
|
class FileInfo {
|
||||||
|
final String path;
|
||||||
|
final String name;
|
||||||
|
final String extension;
|
||||||
|
final int size;
|
||||||
|
final DateTime modifiedTime;
|
||||||
|
final bool exists;
|
||||||
|
|
||||||
|
FileInfo({
|
||||||
|
required this.path,
|
||||||
|
required this.name,
|
||||||
|
required this.extension,
|
||||||
|
required this.size,
|
||||||
|
required this.modifiedTime,
|
||||||
|
required this.exists,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FileInfo.fromFile(File file, [String? filePath]) {
|
||||||
|
final path = filePath ?? file.path;
|
||||||
|
final stat = file.statSync();
|
||||||
|
|
||||||
|
return FileInfo(
|
||||||
|
path: path,
|
||||||
|
name: file.uri.pathSegments.last,
|
||||||
|
extension: FileUtils.getFileExtension(path),
|
||||||
|
size: stat.size,
|
||||||
|
modifiedTime: stat.modified,
|
||||||
|
exists: stat.type != FileSystemEntityType.notFound,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get sizeFormatted => FileUtils.formatFileSize(size);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FileInfo{path: $path, name: $name, size: $sizeFormatted, modified: $modifiedTime}';
|
||||||
|
}
|
||||||
|
}
|
||||||
474
lib/core/utils/image_utils.dart
Normal file
474
lib/core/utils/image_utils.dart
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
import '../constants/app_constants.dart';
|
||||||
|
import '../errors/app_error.dart';
|
||||||
|
import 'logger.dart';
|
||||||
|
|
||||||
|
/// 图片处理工具类
|
||||||
|
/// 提供图片压缩、格式转换、缩略图生成等功能
|
||||||
|
class ImageUtils {
|
||||||
|
// 私有构造函数,防止实例化
|
||||||
|
ImageUtils._();
|
||||||
|
|
||||||
|
static const Uuid _uuid = Uuid();
|
||||||
|
|
||||||
|
/// 生成缩略图
|
||||||
|
/// [imagePath] 原图路径
|
||||||
|
/// [targetPath] 缩略图保存路径
|
||||||
|
/// [maxSize] 缩略图最大尺寸(长边)
|
||||||
|
/// [quality] 压缩质量(0-100)
|
||||||
|
static Future<String> generateThumbnail({
|
||||||
|
required String imagePath,
|
||||||
|
required String targetPath,
|
||||||
|
int maxSize = AppConstants.maxThumbnailSize,
|
||||||
|
int quality = AppConstants.thumbnailQuality,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
Logger.debug('开始生成缩略图: $imagePath');
|
||||||
|
|
||||||
|
// 检查原文件是否存在
|
||||||
|
final originalFile = File(imagePath);
|
||||||
|
if (!await originalFile.exists()) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '原图文件不存在: $imagePath',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
final fileSize = await originalFile.length();
|
||||||
|
Logger.debug('原图文件大小: ${fileSize / 1024 / 1024}MB');
|
||||||
|
|
||||||
|
// 检查文件大小限制
|
||||||
|
if (fileSize > AppConstants.maxImageSize) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '图片文件过大,最大支持30MB',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据文件类型选择处理方式
|
||||||
|
final fileExtension = path.extension(imagePath).toLowerCase();
|
||||||
|
|
||||||
|
if (fileExtension == '.gif') {
|
||||||
|
return await _generateGifThumbnail(
|
||||||
|
imagePath: imagePath,
|
||||||
|
targetPath: targetPath,
|
||||||
|
maxSize: maxSize,
|
||||||
|
quality: quality,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await _generateStaticImageThumbnail(
|
||||||
|
imagePath: imagePath,
|
||||||
|
targetPath: targetPath,
|
||||||
|
maxSize: maxSize,
|
||||||
|
quality: quality,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('生成缩略图失败', error: error, stackTrace: stackTrace);
|
||||||
|
|
||||||
|
if (error is ImageProcessingError) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '生成缩略图失败: ${error.toString()}',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成静态图片缩略图
|
||||||
|
static Future<String> _generateStaticImageThumbnail({
|
||||||
|
required String imagePath,
|
||||||
|
required String targetPath,
|
||||||
|
required int maxSize,
|
||||||
|
required int quality,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 使用 flutter_image_compress 进行压缩
|
||||||
|
final result = await FlutterImageCompress.compressAndGetFile(
|
||||||
|
imagePath,
|
||||||
|
targetPath,
|
||||||
|
quality: quality,
|
||||||
|
minWidth: maxSize,
|
||||||
|
minHeight: maxSize,
|
||||||
|
format: CompressFormat.webp,
|
||||||
|
keepExif: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '图片压缩失败,返回结果为空',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug('静态图片缩略图生成成功: ${result.path}');
|
||||||
|
return result.path;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('静态图片压缩失败', error: error, stackTrace: stackTrace);
|
||||||
|
|
||||||
|
// 如果 flutter_image_compress 失败,使用 image 库作为备选方案
|
||||||
|
return await _generateThumbnailWithImageLib(
|
||||||
|
imagePath: imagePath,
|
||||||
|
targetPath: targetPath,
|
||||||
|
maxSize: maxSize,
|
||||||
|
quality: quality,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用 image 库生成缩略图(备选方案)
|
||||||
|
static Future<String> _generateThumbnailWithImageLib({
|
||||||
|
required String imagePath,
|
||||||
|
required String targetPath,
|
||||||
|
required int maxSize,
|
||||||
|
required int quality,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 读取原图
|
||||||
|
final originalImage = img.decodeImage(await File(imagePath).readAsBytes());
|
||||||
|
if (originalImage == null) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '无法解码图片文件',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算缩放比例
|
||||||
|
final originalWidth = originalImage.width;
|
||||||
|
final originalHeight = originalImage.height;
|
||||||
|
double scale = 1.0;
|
||||||
|
|
||||||
|
if (originalWidth > originalHeight) {
|
||||||
|
// 横图,以宽度为基准
|
||||||
|
if (originalWidth > maxSize) {
|
||||||
|
scale = maxSize / originalWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 竖图或方图,以高度为基准
|
||||||
|
if (originalHeight > maxSize) {
|
||||||
|
scale = maxSize / originalHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetWidth = (originalWidth * scale).round();
|
||||||
|
final targetHeight = (originalHeight * scale).round();
|
||||||
|
|
||||||
|
Logger.debug('图片缩放: ${originalWidth}x$originalHeight -> ${targetWidth}x$targetHeight');
|
||||||
|
|
||||||
|
// 缩放图片
|
||||||
|
final resizedImage = img.copyResize(
|
||||||
|
originalImage,
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 保存为JPEG格式(简化处理)
|
||||||
|
final jpegData = img.encodeJpg(resizedImage, quality: quality);
|
||||||
|
final thumbnailFile = File(targetPath);
|
||||||
|
await thumbnailFile.writeAsBytes(jpegData);
|
||||||
|
|
||||||
|
Logger.debug('使用 image 库生成缩略图成功: $targetPath');
|
||||||
|
return targetPath;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '图片库处理失败: ${error.toString()}',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成GIF缩略图
|
||||||
|
static Future<String> _generateGifThumbnail({
|
||||||
|
required String imagePath,
|
||||||
|
required String targetPath,
|
||||||
|
required int maxSize,
|
||||||
|
required int quality,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
Logger.debug('处理GIF缩略图: $imagePath');
|
||||||
|
|
||||||
|
// 读取GIF文件
|
||||||
|
final gifFile = File(imagePath);
|
||||||
|
final gifBytes = await gifFile.readAsBytes();
|
||||||
|
|
||||||
|
// 解码GIF
|
||||||
|
final gifImage = img.decodeGif(gifBytes);
|
||||||
|
if (gifImage == null) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '无法解码GIF文件',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于大GIF文件,只提取第一帧作为缩略图
|
||||||
|
if (gifImage.length > 1) {
|
||||||
|
Logger.debug('GIF文件包含 ${gifImage.length} 帧,提取第一帧作为缩略图');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取第一帧
|
||||||
|
final firstFrame = gifImage.frames.isNotEmpty ? gifImage.frames.first : null;
|
||||||
|
|
||||||
|
if (firstFrame == null) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '无法获取GIF的第一帧',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算缩放比例
|
||||||
|
final originalWidth = firstFrame.width;
|
||||||
|
final originalHeight = firstFrame.height;
|
||||||
|
double scale = 1.0;
|
||||||
|
|
||||||
|
if (originalWidth > originalHeight) {
|
||||||
|
if (originalWidth > maxSize) {
|
||||||
|
scale = maxSize / originalWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (originalHeight > maxSize) {
|
||||||
|
scale = maxSize / originalHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetWidth = (originalWidth * scale).round();
|
||||||
|
final targetHeight = (originalHeight * scale).round();
|
||||||
|
|
||||||
|
// 缩放第一帧
|
||||||
|
final resizedFrame = img.copyResize(
|
||||||
|
firstFrame,
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 保存为JPEG格式
|
||||||
|
final jpegData = img.encodeJpg(resizedFrame, quality: quality);
|
||||||
|
final thumbnailFile = File(targetPath);
|
||||||
|
await thumbnailFile.writeAsBytes(jpegData);
|
||||||
|
|
||||||
|
Logger.debug('GIF缩略图生成成功: $targetPath');
|
||||||
|
return targetPath;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: 'GIF缩略图生成失败: ${error.toString()}',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成图片的唯一文件名
|
||||||
|
/// [extension] 文件扩展名(包含点号)
|
||||||
|
static String generateUniqueFileName(String extension) {
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final uuid = _uuid.v4().substring(0, 8);
|
||||||
|
return '${timestamp}_$uuid$extension';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据MIME类型获取文件扩展名
|
||||||
|
static String getExtensionFromMimeType(String mimeType) {
|
||||||
|
final mimeTypeMap = {
|
||||||
|
'image/jpeg': '.jpg',
|
||||||
|
'image/jpg': '.jpg',
|
||||||
|
'image/png': '.png',
|
||||||
|
'image/gif': '.gif',
|
||||||
|
'image/webp': '.webp',
|
||||||
|
'image/heic': '.heic',
|
||||||
|
'image/heif': '.heif',
|
||||||
|
};
|
||||||
|
|
||||||
|
return mimeTypeMap[mimeType.toLowerCase()] ?? '.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取图片的基本信息
|
||||||
|
static Future<Map<String, dynamic>> getImageInfo(String imagePath) async {
|
||||||
|
try {
|
||||||
|
final file = File(imagePath);
|
||||||
|
if (!await file.exists()) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '图片文件不存在: $imagePath',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final image = img.decodeImage(bytes);
|
||||||
|
|
||||||
|
if (image == null) {
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '无法解码图片文件',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'width': image.width,
|
||||||
|
'height': image.height,
|
||||||
|
'size': bytes.length,
|
||||||
|
'format': path.extension(imagePath).toLowerCase(),
|
||||||
|
'mimeType': _getMimeType(imagePath),
|
||||||
|
};
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Logger.error('获取图片信息失败', error: error, stackTrace: stackTrace);
|
||||||
|
|
||||||
|
if (error is ImageProcessingError) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ImageProcessingError(
|
||||||
|
message: '获取图片信息失败: ${error.toString()}',
|
||||||
|
code: 'IMAGE_PROCESSING_ERROR',
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据文件路径获取MIME类型
|
||||||
|
static String _getMimeType(String filePath) {
|
||||||
|
final extension = path.extension(filePath).toLowerCase();
|
||||||
|
final mimeTypeMap = {
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.heic': 'image/heic',
|
||||||
|
'.heif': 'image/heif',
|
||||||
|
};
|
||||||
|
|
||||||
|
return mimeTypeMap[extension] ?? 'image/jpeg';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查图片文件是否有效
|
||||||
|
static Future<bool> isValidImage(String imagePath) async {
|
||||||
|
try {
|
||||||
|
final file = File(imagePath);
|
||||||
|
if (!await file.exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fileSize = await file.length();
|
||||||
|
if (fileSize == 0 || fileSize > AppConstants.maxImageSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final extension = path.extension(imagePath).toLowerCase();
|
||||||
|
const validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.heif'];
|
||||||
|
|
||||||
|
if (!validExtensions.contains(extension)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解码图片
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final image = img.decodeImage(bytes);
|
||||||
|
|
||||||
|
return image != null;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('图片验证失败', error: error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 计算图片的宽高比
|
||||||
|
static double calculateAspectRatio(int width, int height) {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
return width / height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据目标宽度计算等比例高度
|
||||||
|
static int calculateHeightForWidth(int originalWidth, int originalHeight, int targetWidth) {
|
||||||
|
if (originalWidth <= 0) {
|
||||||
|
return originalHeight;
|
||||||
|
}
|
||||||
|
return (originalHeight * targetWidth / originalWidth).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据目标高度计算等比例宽度
|
||||||
|
static int calculateWidthForHeight(int originalWidth, int originalHeight, int targetHeight) {
|
||||||
|
if (originalHeight <= 0) {
|
||||||
|
return originalWidth;
|
||||||
|
}
|
||||||
|
return (originalWidth * targetHeight / originalHeight).round();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 图片处理结果
|
||||||
|
class ImageProcessingResult {
|
||||||
|
final String originalPath;
|
||||||
|
final String thumbnailPath;
|
||||||
|
final Map<String, dynamic> metadata;
|
||||||
|
final bool success;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
ImageProcessingResult({
|
||||||
|
required this.originalPath,
|
||||||
|
required this.thumbnailPath,
|
||||||
|
required this.metadata,
|
||||||
|
this.success = true,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ImageProcessingResult.error(String originalPath, String errorMessage) {
|
||||||
|
return ImageProcessingResult(
|
||||||
|
originalPath: originalPath,
|
||||||
|
thumbnailPath: '',
|
||||||
|
metadata: {},
|
||||||
|
success: false,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 支持的图片格式
|
||||||
|
class SupportedImageFormats {
|
||||||
|
static const List<String> formats = [
|
||||||
|
'jpg',
|
||||||
|
'jpeg',
|
||||||
|
'png',
|
||||||
|
'gif',
|
||||||
|
'webp',
|
||||||
|
'heic',
|
||||||
|
'heif',
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<String> extensions = [
|
||||||
|
'.jpg',
|
||||||
|
'.jpeg',
|
||||||
|
'.png',
|
||||||
|
'.gif',
|
||||||
|
'.webp',
|
||||||
|
'.heic',
|
||||||
|
'.heif',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 检查文件扩展名是否支持
|
||||||
|
static bool isSupported(String filePath) {
|
||||||
|
final extension = path.extension(filePath).toLowerCase();
|
||||||
|
return extensions.contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查MIME类型是否支持
|
||||||
|
static bool isMimeTypeSupported(String mimeType) {
|
||||||
|
final supportedMimeTypes = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/webp',
|
||||||
|
'image/heic',
|
||||||
|
'image/heif',
|
||||||
|
];
|
||||||
|
return supportedMimeTypes.contains(mimeType.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
260
lib/core/utils/logger.dart
Normal file
260
lib/core/utils/logger.dart
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// 日志级别枚举
|
||||||
|
enum LogLevel {
|
||||||
|
debug('DEBUG', 0),
|
||||||
|
info('INFO', 1),
|
||||||
|
warning('WARNING', 2),
|
||||||
|
error('ERROR', 3),
|
||||||
|
fatal('FATAL', 4);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final int level;
|
||||||
|
|
||||||
|
const LogLevel(this.name, this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 日志工具类
|
||||||
|
/// 提供统一的日志记录功能,支持不同的日志级别
|
||||||
|
class Logger {
|
||||||
|
// 私有构造函数,防止实例化
|
||||||
|
Logger._();
|
||||||
|
|
||||||
|
/// 当前日志级别(开发模式下为DEBUG,生产模式下为INFO)
|
||||||
|
static LogLevel _currentLevel = kDebugMode ? LogLevel.debug : LogLevel.info;
|
||||||
|
|
||||||
|
/// 日志标签
|
||||||
|
static const String _tag = 'InspoSnap';
|
||||||
|
|
||||||
|
/// 设置日志级别
|
||||||
|
static void setLogLevel(LogLevel level) {
|
||||||
|
_currentLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否应该记录该级别的日志
|
||||||
|
static bool _shouldLog(LogLevel level) {
|
||||||
|
return level.level >= _currentLevel.level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录调试日志
|
||||||
|
static void debug(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
if (_shouldLog(LogLevel.debug)) {
|
||||||
|
_log(LogLevel.debug, message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录信息日志
|
||||||
|
static void info(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
if (_shouldLog(LogLevel.info)) {
|
||||||
|
_log(LogLevel.info, message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录警告日志
|
||||||
|
static void warning(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
if (_shouldLog(LogLevel.warning)) {
|
||||||
|
_log(LogLevel.warning, message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录错误日志
|
||||||
|
static void error(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
if (_shouldLog(LogLevel.error)) {
|
||||||
|
_log(LogLevel.error, message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录致命错误日志
|
||||||
|
static void fatal(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
if (_shouldLog(LogLevel.fatal)) {
|
||||||
|
_log(LogLevel.fatal, message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部日志记录方法
|
||||||
|
static void _log(
|
||||||
|
LogLevel level,
|
||||||
|
String message, {
|
||||||
|
Object? error,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
}) {
|
||||||
|
final logMessage = '[$level.name] $message';
|
||||||
|
|
||||||
|
// 开发环境下使用 developer.log
|
||||||
|
if (kDebugMode) {
|
||||||
|
developer.log(
|
||||||
|
logMessage,
|
||||||
|
name: _tag,
|
||||||
|
time: DateTime.now(),
|
||||||
|
level: level.level,
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境下可以考虑发送到日志服务器
|
||||||
|
if (!kDebugMode && level.level >= LogLevel.error.level) {
|
||||||
|
// TODO: 在生产环境下实现日志上报功能
|
||||||
|
// _reportToServer(level, message, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录方法进入(用于调试)
|
||||||
|
static void enter(String methodName, [Map<String, dynamic>? parameters]) {
|
||||||
|
if (_shouldLog(LogLevel.debug)) {
|
||||||
|
final message = parameters != null
|
||||||
|
? 'Entering $methodName with parameters: $parameters'
|
||||||
|
: 'Entering $methodName';
|
||||||
|
debug(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录方法退出(用于调试)
|
||||||
|
static void exit(String methodName, [dynamic result]) {
|
||||||
|
if (_shouldLog(LogLevel.debug)) {
|
||||||
|
final message = result != null
|
||||||
|
? 'Exiting $methodName with result: $result'
|
||||||
|
: 'Exiting $methodName';
|
||||||
|
debug(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录方法执行时间
|
||||||
|
static Future<T> measureTime<T>(
|
||||||
|
String operationName,
|
||||||
|
Future<T> Function() operation,
|
||||||
|
) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.enter(operationName);
|
||||||
|
final result = await operation();
|
||||||
|
stopwatch.stop();
|
||||||
|
Logger.exit(operationName);
|
||||||
|
Logger.info('$operationName completed in ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
return result;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
stopwatch.stop();
|
||||||
|
Logger.error('$operationName failed after ${stopwatch.elapsedMilliseconds}ms',
|
||||||
|
error: error, stackTrace: stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录同步方法执行时间
|
||||||
|
static T measureSyncTime<T>(
|
||||||
|
String operationName,
|
||||||
|
T Function() operation,
|
||||||
|
) {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.enter(operationName);
|
||||||
|
final result = operation();
|
||||||
|
stopwatch.stop();
|
||||||
|
Logger.exit(operationName);
|
||||||
|
Logger.info('$operationName completed in ${stopwatch.elapsedMilliseconds}ms');
|
||||||
|
return result;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
stopwatch.stop();
|
||||||
|
Logger.error('$operationName failed after ${stopwatch.elapsedMilliseconds}ms',
|
||||||
|
error: error, stackTrace: stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录对象状态(用于调试)
|
||||||
|
static void dumpObject(String objectName, Object object) {
|
||||||
|
if (_shouldLog(LogLevel.debug)) {
|
||||||
|
debug('$objectName state: ${object.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录异常信息
|
||||||
|
static void logException(
|
||||||
|
String context,
|
||||||
|
Object error, {
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
bool isFatal = false,
|
||||||
|
}) {
|
||||||
|
final logLevel = isFatal ? LogLevel.fatal : LogLevel.error;
|
||||||
|
final message = 'Exception in $context: ${error.toString()}';
|
||||||
|
|
||||||
|
_log(logLevel, message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清理日志(如果有本地日志文件)
|
||||||
|
static Future<void> clearLogs() async {
|
||||||
|
// TODO: 如果有本地日志文件,实现清理逻辑
|
||||||
|
info('Logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取日志级别名称
|
||||||
|
static String getLevelName(LogLevel level) {
|
||||||
|
return level.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否处于调试模式
|
||||||
|
static bool get isDebugMode => kDebugMode;
|
||||||
|
|
||||||
|
/// 检查是否处于发布模式
|
||||||
|
static bool get isReleaseMode => kReleaseMode;
|
||||||
|
|
||||||
|
/// 获取当前日志级别
|
||||||
|
static LogLevel get currentLevel => _currentLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 日志混入类,方便在类中使用日志功能
|
||||||
|
mixin LoggerMixin {
|
||||||
|
/// 记录调试日志
|
||||||
|
void logDebug(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
Logger.debug(message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录信息日志
|
||||||
|
void logInfo(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
Logger.info(message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录警告日志
|
||||||
|
void logWarning(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
Logger.warning(message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录错误日志
|
||||||
|
void logError(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
Logger.error(message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录致命错误日志
|
||||||
|
void logFatal(String message, {Object? error, StackTrace? stackTrace}) {
|
||||||
|
Logger.fatal(message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录方法进入(用于调试)
|
||||||
|
void logEnter(String methodName, [Map<String, dynamic>? parameters]) {
|
||||||
|
Logger.enter('$runtimeType.$methodName', parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录方法退出(用于调试)
|
||||||
|
void logExit(String methodName, [dynamic result]) {
|
||||||
|
Logger.exit('$runtimeType.$methodName', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录方法执行时间
|
||||||
|
Future<T> logMeasureTime<T>(
|
||||||
|
String operationName,
|
||||||
|
Future<T> Function() operation,
|
||||||
|
) async {
|
||||||
|
return await Logger.measureTime('$runtimeType.$operationName', operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录同步方法执行时间
|
||||||
|
T logMeasureSyncTime<T>(
|
||||||
|
String operationName,
|
||||||
|
T Function() operation,
|
||||||
|
) {
|
||||||
|
return Logger.measureSyncTime('$runtimeType.$operationName', operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,74 +1,35 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'pages/photo_page.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'pages/categories_page.dart';
|
import 'core/constants/app_constants.dart';
|
||||||
import 'pages/add_photo_page.dart';
|
import 'core/utils/logger.dart';
|
||||||
|
import 'presentation/app_widget.dart';
|
||||||
|
|
||||||
void main() {
|
/// 应用程序主入口
|
||||||
runApp(const SnapWishApp());
|
/// 负责初始化应用并配置全局设置
|
||||||
}
|
void main() async {
|
||||||
|
// 确保Flutter绑定已初始化
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
class SnapWishApp extends StatelessWidget {
|
// 初始化日志系统
|
||||||
const SnapWishApp({super.key});
|
Logger.setLogLevel(LogLevel.info);
|
||||||
|
Logger.info('应用启动 - ${AppConstants.appName} v${AppConstants.appVersion}');
|
||||||
|
|
||||||
@override
|
// 捕获并记录未处理的异常
|
||||||
Widget build(BuildContext context) {
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
return MaterialApp(
|
Logger.logException('Flutter Error', details.exception, stackTrace: details.stack);
|
||||||
title: 'SnapWish',
|
FlutterError.presentError(details);
|
||||||
theme: ThemeData(
|
};
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
home: const MainPage(),
|
|
||||||
routes: {
|
|
||||||
'/add-photo': (context) => const AddPhotoPage(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
// 运行应用
|
||||||
const MainPage({super.key});
|
runApp(
|
||||||
|
const ProviderScope(
|
||||||
@override
|
child: AppWidget(),
|
||||||
State<MainPage> createState() => _MainPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MainPageState extends State<MainPage> {
|
|
||||||
int _currentIndex = 0;
|
|
||||||
|
|
||||||
final List<Widget> _pages = [
|
|
||||||
const PhotoPage(),
|
|
||||||
const CategoriesPage(),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: _pages[_currentIndex],
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pushNamed(context, '/add-photo');
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: NavigationBar(
|
|
||||||
selectedIndex: _currentIndex,
|
|
||||||
onDestinationSelected: (index) {
|
|
||||||
setState(() {
|
|
||||||
_currentIndex = index;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
destinations: const [
|
|
||||||
NavigationDestination(
|
|
||||||
icon: Icon(Icons.photo_library),
|
|
||||||
label: '照片',
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
|
||||||
icon: Icon(Icons.folder),
|
|
||||||
label: '分类',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 全局错误处理
|
||||||
|
/// 捕获并记录未处理的异步异常
|
||||||
|
void handleGlobalError(Object error, StackTrace stackTrace) {
|
||||||
|
Logger.logException('Global Error', error, stackTrace: stackTrace, isFatal: true);
|
||||||
}
|
}
|
||||||
@ -1,170 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class AddPhotoPage extends StatefulWidget {
|
|
||||||
const AddPhotoPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AddPhotoPage> createState() => _AddPhotoPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddPhotoPageState extends State<AddPhotoPage> {
|
|
||||||
String? _selectedCategory;
|
|
||||||
final TextEditingController _tagsController = TextEditingController();
|
|
||||||
final List<String> _tags = [];
|
|
||||||
|
|
||||||
// TODO: 替换为真实文件夹数据
|
|
||||||
final List<String> _categories = ['默认分类', '人像', '风景', '建筑'];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_tagsController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectPhoto() {
|
|
||||||
// TODO: 实现选择本地照片功能
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('选择照片功能待实现')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addTag(String text) {
|
|
||||||
final tag = text.trim();
|
|
||||||
if (tag.isNotEmpty && !_tags.contains(tag)) {
|
|
||||||
setState(() {
|
|
||||||
_tags.add(tag);
|
|
||||||
});
|
|
||||||
_tagsController.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeTag(String tag) {
|
|
||||||
setState(() {
|
|
||||||
_tags.remove(tag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _savePhoto() {
|
|
||||||
if (_selectedCategory == null) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('请选择文件夹')),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 实现保存照片功能
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('照片已保存到 $_selectedCategory')),
|
|
||||||
);
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('添加照片'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: _savePhoto,
|
|
||||||
child: const Text('保存', style: TextStyle(color: Colors.white)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// 照片预览区域
|
|
||||||
GestureDetector(
|
|
||||||
onTap: _selectPhoto,
|
|
||||||
child: Container(
|
|
||||||
height: 200,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.grey[400]!),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Text('点击选择照片', style: TextStyle(color: Colors.grey)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// 文件夹选择
|
|
||||||
const Text('选择文件夹', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
DropdownButtonFormField<String>(
|
|
||||||
value: _selectedCategory,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
hintText: '请选择文件夹',
|
|
||||||
),
|
|
||||||
items: _categories.map((category) {
|
|
||||||
return DropdownMenuItem(
|
|
||||||
value: category,
|
|
||||||
child: Text(category),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_selectedCategory = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// 标签输入
|
|
||||||
const Text('添加标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextField(
|
|
||||||
controller: _tagsController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
hintText: '输入标签(支持 #标签 格式)',
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
onPressed: () => _addTag(_tagsController.text),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onSubmitted: _addTag,
|
|
||||||
onChanged: (value) {
|
|
||||||
// 处理 # 开头的标签输入
|
|
||||||
if (value.endsWith(' ') || value.endsWith('#')) {
|
|
||||||
final parts = value.split('#');
|
|
||||||
if (parts.length > 1) {
|
|
||||||
final tag = parts.last.trim();
|
|
||||||
if (tag.isNotEmpty) {
|
|
||||||
_addTag(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// 标签列表
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 4,
|
|
||||||
children: _tags.map((tag) => Chip(
|
|
||||||
label: Text('#$tag'),
|
|
||||||
onDeleted: () => _removeTag(tag),
|
|
||||||
deleteIcon: const Icon(Icons.close, size: 16),
|
|
||||||
)).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class CategoriesPage extends StatefulWidget {
|
|
||||||
const CategoriesPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CategoriesPage> createState() => _CategoriesPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CategoriesPageState extends State<CategoriesPage> {
|
|
||||||
// TODO: 替换为真实文件夹数据
|
|
||||||
final List<Map<String, dynamic>> _categories = [
|
|
||||||
{'name': '默认分类', 'count': 15, 'isDefault': true},
|
|
||||||
{'name': '人像', 'count': 8, 'isDefault': false},
|
|
||||||
{'name': '风景', 'count': 12, 'isDefault': false},
|
|
||||||
{'name': '建筑', 'count': 5, 'isDefault': false},
|
|
||||||
];
|
|
||||||
|
|
||||||
void _createCategory() {
|
|
||||||
final controller = TextEditingController();
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('创建新文件夹'),
|
|
||||||
content: TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: '输入文件夹名称',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('取消'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (controller.text.isNotEmpty) {
|
|
||||||
setState(() {
|
|
||||||
_categories.add({
|
|
||||||
'name': controller.text,
|
|
||||||
'count': 0,
|
|
||||||
'isDefault': false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('创建'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteCategory(int index) {
|
|
||||||
final category = _categories[index];
|
|
||||||
if (category['isDefault']) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('默认分类不能删除')),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('删除文件夹'),
|
|
||||||
content: Text('确定要删除"${category['name']}"吗?\n${category['count']}张照片将被移至默认分类。'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('取消'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
// 将照片移至默认分类
|
|
||||||
_categories[0]['count'] += category['count'];
|
|
||||||
_categories.removeAt(index);
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
||||||
child: const Text('删除'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('分类'),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.create_new_folder),
|
|
||||||
onPressed: _createCategory,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
itemCount: _categories.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final category = _categories[index];
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: category['isDefault']
|
|
||||||
? Colors.blue
|
|
||||||
: Colors.deepPurple,
|
|
||||||
child: const Icon(Icons.folder, color: Colors.white),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
category['name'],
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
subtitle: Text('${category['count']} 张照片'),
|
|
||||||
trailing: category['isDefault']
|
|
||||||
? null
|
|
||||||
: IconButton(
|
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
|
||||||
onPressed: () => _deleteCategory(index),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
// TODO: 打开该分类的照片列表
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('打开 ${category['name']}')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class PhotoPage extends StatelessWidget {
|
|
||||||
const PhotoPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('照片'),
|
|
||||||
),
|
|
||||||
body: const PhotoGrid(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PhotoGrid extends StatelessWidget {
|
|
||||||
const PhotoGrid({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// TODO: 替换为真实照片数据
|
|
||||||
final mockPhotos = List.generate(20, (index) => '照片 ${index + 1}');
|
|
||||||
|
|
||||||
return GridView.builder(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 3,
|
|
||||||
crossAxisSpacing: 4,
|
|
||||||
mainAxisSpacing: 4,
|
|
||||||
childAspectRatio: 1,
|
|
||||||
),
|
|
||||||
itemCount: mockPhotos.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => PhotoDetailPage(
|
|
||||||
photos: mockPhotos,
|
|
||||||
initialIndex: index,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.photo, size: 40, color: Colors.grey),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
mockPhotos[index],
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PhotoDetailPage extends StatefulWidget {
|
|
||||||
final List<String> photos;
|
|
||||||
final int initialIndex;
|
|
||||||
|
|
||||||
const PhotoDetailPage({
|
|
||||||
super.key,
|
|
||||||
required this.photos,
|
|
||||||
required this.initialIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PhotoDetailPage> createState() => _PhotoDetailPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PhotoDetailPageState extends State<PhotoDetailPage> {
|
|
||||||
late int _currentIndex;
|
|
||||||
late PageController _pageController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_currentIndex = widget.initialIndex;
|
|
||||||
_pageController = PageController(initialPage: _currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_pageController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.favorite_border),
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: 实现收藏功能
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('收藏功能待实现')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: 实现分享功能
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('分享功能待实现')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: 实现删除功能
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('删除功能待实现')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
PageView.builder(
|
|
||||||
controller: _pageController,
|
|
||||||
itemCount: widget.photos.length,
|
|
||||||
onPageChanged: (index) {
|
|
||||||
setState(() {
|
|
||||||
_currentIndex = index;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: MediaQuery.of(context).size.height * 0.7,
|
|
||||||
color: Colors.grey[800],
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.photo, size: 100, color: Colors.white),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
widget.photos[index],
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 20,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
'${_currentIndex + 1} / ${widget.photos.length}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
283
lib/presentation/app_widget.dart
Normal file
283
lib/presentation/app_widget.dart
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../core/constants/app_constants.dart';
|
||||||
|
import '../core/theme/app_theme.dart';
|
||||||
|
import '../core/utils/logger.dart';
|
||||||
|
|
||||||
|
/// 主应用组件
|
||||||
|
/// 负责配置MaterialApp和全局设置
|
||||||
|
class AppWidget extends ConsumerWidget {
|
||||||
|
const AppWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
Logger.debug('构建AppWidget');
|
||||||
|
|
||||||
|
return MaterialApp(
|
||||||
|
// 应用基础配置
|
||||||
|
title: AppConstants.appName,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
|
||||||
|
// 主题配置
|
||||||
|
theme: AppTheme.lightTheme,
|
||||||
|
darkTheme: AppTheme.darkTheme,
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
|
|
||||||
|
// 本地化配置
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [
|
||||||
|
Locale('zh', 'CN'), // 简体中文
|
||||||
|
Locale('zh', 'TW'), // 繁体中文
|
||||||
|
Locale('en', 'US'), // English
|
||||||
|
],
|
||||||
|
|
||||||
|
// 主页配置
|
||||||
|
home: const AppHomePage(),
|
||||||
|
|
||||||
|
// 导航观察者
|
||||||
|
navigatorObservers: [
|
||||||
|
_AppNavigatorObserver(),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 构建器配置(用于全局错误处理)
|
||||||
|
builder: (context, child) {
|
||||||
|
// 配置文字缩放因子,确保文字大小一致性
|
||||||
|
final mediaQueryData = MediaQuery.of(context);
|
||||||
|
final textScaler = MediaQuery.of(context).textScaler;
|
||||||
|
final scaleFactor = textScaler.scale(1.0).clamp(0.8, 1.2);
|
||||||
|
|
||||||
|
return MediaQuery(
|
||||||
|
data: mediaQueryData.copyWith(
|
||||||
|
textScaler: TextScaler.linear(scaleFactor),
|
||||||
|
),
|
||||||
|
child: child ?? const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 滚动行为配置
|
||||||
|
scrollBehavior: const MaterialScrollBehavior().copyWith(
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用主页组件
|
||||||
|
/// 作为占位符,后续会替换为实际的主页
|
||||||
|
class AppHomePage extends StatefulWidget {
|
||||||
|
const AppHomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppHomePage> createState() => _AppHomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppHomePageState extends State<AppHomePage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
Logger.debug('AppHomePage初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
Logger.debug('构建AppHomePage');
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('想拍'),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 应用Logo(占位符)
|
||||||
|
Icon(
|
||||||
|
Icons.photo_library,
|
||||||
|
size: 120,
|
||||||
|
color: theme.colorScheme.primary.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// 欢迎文字
|
||||||
|
Text(
|
||||||
|
'欢迎使用想拍',
|
||||||
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 副标题
|
||||||
|
Text(
|
||||||
|
'Shoot What Inspires You',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
|
// 功能说明
|
||||||
|
Text(
|
||||||
|
'从其他应用分享图片到这里,\n开始收集你的灵感吧!',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
|
// 开始使用按钮
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: 导航到图库页面
|
||||||
|
Logger.debug('点击开始使用按钮');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.arrow_forward),
|
||||||
|
label: const Text('开始使用'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 底部导航栏(占位符)
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
destinations: const [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.photo_library),
|
||||||
|
label: '图库',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.folder),
|
||||||
|
label: '文件夹',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.label),
|
||||||
|
label: '标签',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.settings),
|
||||||
|
label: '设置',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onDestinationSelected: (index) {
|
||||||
|
Logger.debug('底部导航选择: $index');
|
||||||
|
// TODO: 实现页面切换逻辑
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导航观察者
|
||||||
|
/// 用于记录页面导航事件
|
||||||
|
class _AppNavigatorObserver extends NavigatorObserver {
|
||||||
|
@override
|
||||||
|
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||||
|
if (route.settings.name != null) {
|
||||||
|
Logger.debug('页面进入: ${route.settings.name}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||||
|
if (route.settings.name != null) {
|
||||||
|
Logger.debug('页面退出: ${route.settings.name}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||||
|
if (route.settings.name != null) {
|
||||||
|
Logger.debug('页面移除: ${route.settings.name}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
|
||||||
|
if (newRoute?.settings.name != null) {
|
||||||
|
Logger.debug('页面替换: ${newRoute!.settings.name}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用配置混入
|
||||||
|
mixin AppConfigMixin {
|
||||||
|
/// 获取应用名称
|
||||||
|
String get appName => AppConstants.appName;
|
||||||
|
|
||||||
|
/// 获取应用版本
|
||||||
|
String get appVersion => AppConstants.appVersion;
|
||||||
|
|
||||||
|
/// 获取应用描述
|
||||||
|
String get appDescription => AppConstants.appDescription;
|
||||||
|
|
||||||
|
/// 检查是否为调试模式
|
||||||
|
bool get isDebugMode => kDebugMode;
|
||||||
|
|
||||||
|
/// 检查是否为发布模式
|
||||||
|
bool get isReleaseMode => kReleaseMode;
|
||||||
|
|
||||||
|
/// 获取当前时间戳
|
||||||
|
int get currentTimestamp => DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
/// 格式化文件大小
|
||||||
|
String formatFileSize(int bytes) {
|
||||||
|
if (bytes <= 0) return '0 B';
|
||||||
|
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
int unitIndex = 0;
|
||||||
|
double size = bytes.toDouble();
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${size.toStringAsFixed(2)} ${units[unitIndex]}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 格式化日期
|
||||||
|
String formatDate(DateTime date) {
|
||||||
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 计算时间差(友好显示)
|
||||||
|
String formatTimeDifference(DateTime from, DateTime to) {
|
||||||
|
final difference = to.difference(from);
|
||||||
|
|
||||||
|
if (difference.inDays > 0) {
|
||||||
|
return '${difference.inDays}天前';
|
||||||
|
} else if (difference.inHours > 0) {
|
||||||
|
return '${difference.inHours}小时前';
|
||||||
|
} else if (difference.inMinutes > 0) {
|
||||||
|
return '${difference.inMinutes}分钟前';
|
||||||
|
} else {
|
||||||
|
return '刚刚';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用混入
|
||||||
|
/// 组合所有应用相关的混入
|
||||||
|
mixin AppMixin on AppConfigMixin, LoggerMixin {
|
||||||
|
// 可以在这里添加更多通用的应用功能
|
||||||
|
}
|
||||||
264
lib/presentation/l10n/app_localizations.dart
Normal file
264
lib/presentation/l10n/app_localizations.dart
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
||||||
|
/// 应用本地化支持类
|
||||||
|
/// 管理多语言翻译和区域设置
|
||||||
|
class AppLocalizations {
|
||||||
|
final Locale locale;
|
||||||
|
|
||||||
|
AppLocalizations(this.locale);
|
||||||
|
|
||||||
|
/// 获取本地化实例
|
||||||
|
static AppLocalizations? of(BuildContext context) {
|
||||||
|
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 本地化委托
|
||||||
|
static const AppLocalizationsDelegate delegate = AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
/// 支持的语言列表
|
||||||
|
static const supportedLocales = [
|
||||||
|
Locale('zh', 'CN'), // 简体中文
|
||||||
|
Locale('zh', 'TW'), // 繁体中文
|
||||||
|
Locale('en', 'US'), // English
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 本地化代理列表
|
||||||
|
static const localizationsDelegates = [
|
||||||
|
AppLocalizationsDelegate(),
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 基础本地化字符串
|
||||||
|
String get appName {
|
||||||
|
switch (locale.languageCode) {
|
||||||
|
case 'zh':
|
||||||
|
return locale.countryCode == 'TW' ? '想拍' : '想拍';
|
||||||
|
case 'en':
|
||||||
|
default:
|
||||||
|
return 'InspoSnap';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get appDescription {
|
||||||
|
switch (locale.languageCode) {
|
||||||
|
case 'zh':
|
||||||
|
return locale.countryCode == 'TW'
|
||||||
|
? '拍攝什麼激發你的靈感'
|
||||||
|
: '拍摄什么激发你的灵感';
|
||||||
|
case 'en':
|
||||||
|
default:
|
||||||
|
return 'Shoot What Inspires You';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航相关
|
||||||
|
String get gallery => _getLocalizedString('图库', '圖庫', 'Gallery');
|
||||||
|
String get folders => _getLocalizedString('文件夹', '資料夾', 'Folders');
|
||||||
|
String get tags => _getLocalizedString('标签', '標籤', 'Tags');
|
||||||
|
String get settings => _getLocalizedString('设置', '設定', 'Settings');
|
||||||
|
|
||||||
|
// 欢迎页面
|
||||||
|
String get welcomeMessage => _getLocalizedString('欢迎使用想拍', '歡迎使用想拍', 'Welcome to InspoSnap');
|
||||||
|
String get welcomeSubtitle => _getLocalizedString('拍摄什么激发你的灵感', '拍攝什麼激發你的靈感', 'Shoot What Inspires You');
|
||||||
|
String get featureDescription => _getLocalizedString(
|
||||||
|
'从其他应用分享图片到这里,\n开始收集你的灵感吧!',
|
||||||
|
'從其他應用分享圖片到這裡,\n開始收集你的靈感吧!',
|
||||||
|
'Share images from other apps here,\nand start collecting your inspirations!',
|
||||||
|
);
|
||||||
|
String get startUsing => _getLocalizedString('开始使用', '開始使用', 'Start Using');
|
||||||
|
|
||||||
|
// 通用操作
|
||||||
|
String get save => _getLocalizedString('保存', '儲存', 'Save');
|
||||||
|
String get cancel => _getLocalizedString('取消', '取消', 'Cancel');
|
||||||
|
String get delete => _getLocalizedString('删除', '刪除', 'Delete');
|
||||||
|
String get edit => _getLocalizedString('编辑', '編輯', 'Edit');
|
||||||
|
String get search => _getLocalizedString('搜索', '搜尋', 'Search');
|
||||||
|
String get share => _getLocalizedString('分享', '分享', 'Share');
|
||||||
|
String get export => _getLocalizedString('导出', '匯出', 'Export');
|
||||||
|
String get confirm => _getLocalizedString('确认', '確認', 'Confirm');
|
||||||
|
String get close => _getLocalizedString('关闭', '關閉', 'Close');
|
||||||
|
|
||||||
|
// 错误消息
|
||||||
|
String get errorTitle => _getLocalizedString('出错了', '出錯了', 'Error');
|
||||||
|
String get errorUnknown => _getLocalizedString('出了点小问题,请稍后重试', '出了點小問題,請稍後重試', 'Something went wrong, please try again later');
|
||||||
|
String get errorNetwork => _getLocalizedString('网络连接异常,请检查网络后重试', '網路連線異常,請檢查網路後重試', 'Network connection error, please check your connection and try again');
|
||||||
|
String get errorStorage => _getLocalizedString('存储空间不足,请清理空间后重试', '儲存空間不足,請清理空間後重試', 'Storage space insufficient, please free up space and try again');
|
||||||
|
String get errorPermission => _getLocalizedString('需要相关权限才能继续使用', '需要相關權限才能繼續使用', 'Related permissions are required to continue');
|
||||||
|
String get errorImageProcessing => _getLocalizedString('图片处理失败,请检查图片格式或重试', '圖片處理失敗,請檢查圖片格式或重試', 'Image processing failed, please check the image format or try again');
|
||||||
|
String get errorShareReceive => _getLocalizedString('出了点小问题,请重新分享试试~', '出了點小問題,請重新分享試試~', 'Something went wrong, please try sharing again~');
|
||||||
|
|
||||||
|
// 成功消息
|
||||||
|
String get successSaved => _getLocalizedString('保存成功', '儲存成功', 'Saved successfully');
|
||||||
|
String get successDeleted => _getLocalizedString('删除成功', '刪除成功', 'Deleted successfully');
|
||||||
|
String get successUpdated => _getLocalizedString('更新成功', '更新成功', 'Updated successfully');
|
||||||
|
|
||||||
|
// 空状态
|
||||||
|
String get emptyGallery => _getLocalizedString('还没有灵感?去其他App分享图片到这里吧~', '還沒有靈感?去其他App分享圖片到這裡吧~', 'No inspirations yet? Go share images from other apps here~');
|
||||||
|
String get emptyFolder => _getLocalizedString('这个文件夹还在等它的第一张灵感图片', '這個文件夾還在等它的第一張靈感圖片', 'This folder is still waiting for its first inspiration image');
|
||||||
|
String get emptySearch => _getLocalizedString('换个关键词试试?或者去收集更多灵感吧!', '換個關鍵詞試試?或者去收集更多靈感吧!', 'Try a different keyword? Or go collect more inspirations!');
|
||||||
|
|
||||||
|
// 主题相关
|
||||||
|
String get themeSystem => _getLocalizedString('跟随系统', '跟隨系統', 'Follow System');
|
||||||
|
String get themeLight => _getLocalizedString('浅色模式', '淺色模式', 'Light Mode');
|
||||||
|
String get themeDark => _getLocalizedString('深色模式', '深色模式', 'Dark Mode');
|
||||||
|
String get themeDescriptionSystem => _getLocalizedString('根据系统设置自动切换浅色和深色模式', '根據系統設定自動切換淺色和深色模式', 'Automatically switch between light and dark mode based on system settings');
|
||||||
|
String get themeDescriptionLight => _getLocalizedString('始终使用浅色模式', '始終使用淺色模式', 'Always use light mode');
|
||||||
|
String get themeDescriptionDark => _getLocalizedString('始终使用深色模式', '始終使用深色模式', 'Always use dark mode');
|
||||||
|
|
||||||
|
// 语言设置
|
||||||
|
String get languageSimplifiedChinese => _getLocalizedString('简体中文', '簡體中文', 'Simplified Chinese');
|
||||||
|
String get languageTraditionalChinese => _getLocalizedString('繁体中文', '繁體中文', 'Traditional Chinese');
|
||||||
|
String get languageEnglish => _getLocalizedString('English', 'English', 'English');
|
||||||
|
|
||||||
|
// 设置相关
|
||||||
|
String get settingsAppearance => _getLocalizedString('外观设置', '外觀設定', 'Appearance');
|
||||||
|
String get settingsStorage => _getLocalizedString('存储管理', '儲存管理', 'Storage Management');
|
||||||
|
String get settingsAbout => _getLocalizedString('关于应用', '關於應用', 'About App');
|
||||||
|
String get settingsLanguage => _getLocalizedString('语言设置', '語言設定', 'Language');
|
||||||
|
String get settingsTheme => _getLocalizedString('主题模式', '主題模式', 'Theme Mode');
|
||||||
|
String get settingsGridLayout => _getLocalizedString('网格布局', '網格佈局', 'Grid Layout');
|
||||||
|
String get settingsStorageUsage => _getLocalizedString('存储使用情况', '儲存使用情況', 'Storage Usage');
|
||||||
|
String get settingsClearCache => _getLocalizedString('清理缓存', '清理緩存', 'Clear Cache');
|
||||||
|
String get settingsVersion => _getLocalizedString('版本信息', '版本資訊', 'Version Info');
|
||||||
|
String get settingsPrivacyPolicy => _getLocalizedString('隐私政策', '隱私政策', 'Privacy Policy');
|
||||||
|
String get settingsTermsOfService => _getLocalizedString('用户协议', '使用者協議', 'Terms of Service');
|
||||||
|
|
||||||
|
// 分享相关
|
||||||
|
String get shareReceiveTitle => _getLocalizedString('保存图片', '儲存圖片', 'Save Image');
|
||||||
|
String get shareReceiveMultiple => _getLocalizedString('共 %d 张图片', '共 %d 張圖片', '%d images');
|
||||||
|
String get shareReceiveFolder => _getLocalizedString('选择文件夹', '選擇文件夾', 'Select Folder');
|
||||||
|
String get shareReceiveTags => _getLocalizedString('添加标签', '新增標籤', 'Add Tags');
|
||||||
|
String get shareReceiveNote => _getLocalizedString('添加备注(可选)', '新增備註(可選)', 'Add Note (Optional)');
|
||||||
|
String get shareReceiveBatchMode => _getLocalizedString('批量应用', '批次應用', 'Batch Apply');
|
||||||
|
String get shareReceiveSingleMode => _getLocalizedString('单张编辑', '單張編輯', 'Single Edit');
|
||||||
|
String get shareReceiveSaveProgress => _getLocalizedString('正在保存 %d/%d', '正在儲存 %d/%d', 'Saving %d/%d');
|
||||||
|
|
||||||
|
// 文件夹相关
|
||||||
|
String get folderUncategorized => _getLocalizedString('未分类', '未分類', 'Uncategorized');
|
||||||
|
String get folderCreateNew => _getLocalizedString('新建文件夹', '新建文件夾', 'New Folder');
|
||||||
|
String get folderNameHint => _getLocalizedString('请输入文件夹名称', '請輸入文件夾名稱', 'Enter folder name');
|
||||||
|
String get folderIcon => _getLocalizedString('选择图标', '選擇圖標', 'Select Icon');
|
||||||
|
String get folderColor => _getLocalizedString('选择颜色', '選擇顏色', 'Select Color');
|
||||||
|
|
||||||
|
// 标签相关
|
||||||
|
String get tagCreateNew => _getLocalizedString('创建标签', '建立標籤', 'Create Tag');
|
||||||
|
String get tagNameHint => _getLocalizedString('请输入标签名称', '請輸入標籤名稱', 'Enter tag name');
|
||||||
|
String get tagMaxLength => _getLocalizedString('标签最多20个字符', '標籤最多20個字元', 'Tag max 20 characters');
|
||||||
|
String get tagDuplicate => _getLocalizedString('标签已存在', '標籤已存在', 'Tag already exists');
|
||||||
|
|
||||||
|
// 图片详情
|
||||||
|
String get imageDetails => _getLocalizedString('图片详情', '圖片詳情', 'Image Details');
|
||||||
|
String get imageInfo => _getLocalizedString('图片信息', '圖片資訊', 'Image Info');
|
||||||
|
String get imageFolder => _getLocalizedString('所属文件夹', '所屬文件夾', 'Folder');
|
||||||
|
String get imageTags => _getLocalizedString('标签', '標籤', 'Tags');
|
||||||
|
String get imageNote => _getLocalizedString('备注', '備註', 'Note');
|
||||||
|
String get imageSize => _getLocalizedString('文件大小', '檔案大小', 'File Size');
|
||||||
|
String get imageDimensions => _getLocalizedString('图片尺寸', '圖片尺寸', 'Dimensions');
|
||||||
|
String get imageFormat => _getLocalizedString('图片格式', '圖片格式', 'Format');
|
||||||
|
String get imageCreated => _getLocalizedString('保存时间', '儲存時間', 'Saved Time');
|
||||||
|
|
||||||
|
// 删除确认
|
||||||
|
String get deleteConfirm => _getLocalizedString('确定要删除吗?', '確定要刪除嗎?', 'Are you sure you want to delete?');
|
||||||
|
String get deleteConfirmImage => _getLocalizedString('删除后无法恢复', '刪除後無法恢復', 'Cannot be recovered after deletion');
|
||||||
|
String get deleteConfirmFolder => _getLocalizedString('文件夹删除后,其中的图片将移至"未分类"', '文件夾刪除後,其中的圖片將移至"未分類"', 'After folder deletion, images will be moved to "Uncategorized"');
|
||||||
|
|
||||||
|
/// 获取本地化字符串的通用方法
|
||||||
|
String _getLocalizedString(String simplified, String traditional, String english) {
|
||||||
|
switch (locale.languageCode) {
|
||||||
|
case 'zh':
|
||||||
|
return locale.countryCode == 'TW' ? traditional : simplified;
|
||||||
|
case 'en':
|
||||||
|
default:
|
||||||
|
return english;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 本地化委托类
|
||||||
|
class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||||
|
const AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isSupported(Locale locale) {
|
||||||
|
// 检查是否支持该locale
|
||||||
|
for (final supportedLocale in AppLocalizations.supportedLocales) {
|
||||||
|
if (supportedLocale.languageCode == locale.languageCode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AppLocalizations> load(Locale locale) async {
|
||||||
|
// 返回对应语言的本地化实例
|
||||||
|
return AppLocalizations(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReload(AppLocalizationsDelegate old) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 本地化扩展方法
|
||||||
|
extension LocalizationsContextExtension on BuildContext {
|
||||||
|
/// 获取本地化实例
|
||||||
|
AppLocalizations? get loc => AppLocalizations.of(this);
|
||||||
|
|
||||||
|
/// 获取当前语言代码
|
||||||
|
String? get languageCode => loc?.locale.languageCode;
|
||||||
|
|
||||||
|
/// 检查是否为中文
|
||||||
|
bool get isChinese => languageCode == 'zh';
|
||||||
|
|
||||||
|
/// 检查是否为英文
|
||||||
|
bool get isEnglish => languageCode == 'en';
|
||||||
|
|
||||||
|
/// 获取文本方向
|
||||||
|
TextDirection get textDirection {
|
||||||
|
return isEnglish ? TextDirection.ltr : TextDirection.ltr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 本地化混入
|
||||||
|
mixin LocalizationsMixin {
|
||||||
|
/// 获取本地化实例
|
||||||
|
AppLocalizations? getLocalization(BuildContext context) {
|
||||||
|
return AppLocalizations.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用名称
|
||||||
|
String getAppName(BuildContext context) {
|
||||||
|
return getLocalization(context)?.appName ?? '想拍';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用描述
|
||||||
|
String getAppDescription(BuildContext context) {
|
||||||
|
return getLocalization(context)?.appDescription ?? '拍摄什么激发你的灵感';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取错误消息
|
||||||
|
String getErrorMessage(BuildContext context, String errorKey) {
|
||||||
|
final localization = getLocalization(context);
|
||||||
|
if (localization == null) return '出了点小问题';
|
||||||
|
|
||||||
|
switch (errorKey) {
|
||||||
|
case 'unknown':
|
||||||
|
return localization.errorUnknown;
|
||||||
|
case 'network':
|
||||||
|
return localization.errorNetwork;
|
||||||
|
case 'storage':
|
||||||
|
return localization.errorStorage;
|
||||||
|
case 'permission':
|
||||||
|
return localization.errorPermission;
|
||||||
|
case 'image_processing':
|
||||||
|
return localization.errorImageProcessing;
|
||||||
|
case 'share_receive':
|
||||||
|
return localization.errorShareReceive;
|
||||||
|
default:
|
||||||
|
return localization.errorUnknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
lib/presentation/providers/theme_provider.dart
Normal file
174
lib/presentation/providers/theme_provider.dart
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../core/utils/logger.dart';
|
||||||
|
|
||||||
|
/// 主题状态提供者
|
||||||
|
final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeMode>((ref) {
|
||||||
|
return ThemeNotifier(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 主题通知器
|
||||||
|
class ThemeNotifier extends StateNotifier<ThemeMode> with LoggerMixin {
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
ThemeNotifier(this.ref) : super(ThemeMode.system) {
|
||||||
|
_initializeTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化主题设置
|
||||||
|
Future<void> _initializeTheme() async {
|
||||||
|
logEnter('_initializeTheme');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 默认使用系统主题
|
||||||
|
state = ThemeMode.system;
|
||||||
|
logInfo('主题初始化完成: $state');
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logError('主题初始化失败', error: error, stackTrace: stackTrace);
|
||||||
|
state = ThemeMode.system;
|
||||||
|
}
|
||||||
|
|
||||||
|
logExit('_initializeTheme');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置主题模式
|
||||||
|
Future<void> setThemeMode(ThemeMode themeMode) async {
|
||||||
|
logEnter('setThemeMode', {'themeMode': themeMode.toString()});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (state == themeMode) {
|
||||||
|
logInfo('主题模式未变化,跳过设置: $themeMode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = themeMode;
|
||||||
|
|
||||||
|
logInfo('主题模式已更新: $themeMode');
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logError('设置主题模式失败', error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
logExit('setThemeMode');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换主题模式
|
||||||
|
Future<void> toggleThemeMode() async {
|
||||||
|
logEnter('toggleThemeMode');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final currentMode = state;
|
||||||
|
ThemeMode newMode;
|
||||||
|
|
||||||
|
switch (currentMode) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
newMode = ThemeMode.light;
|
||||||
|
break;
|
||||||
|
case ThemeMode.light:
|
||||||
|
newMode = ThemeMode.dark;
|
||||||
|
break;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
newMode = ThemeMode.system;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setThemeMode(newMode);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logError('切换主题模式失败', error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
logExit('toggleThemeMode');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前主题模式显示名称
|
||||||
|
String getCurrentThemeModeName() {
|
||||||
|
switch (state) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
return '跟随系统';
|
||||||
|
case ThemeMode.light:
|
||||||
|
return '浅色模式';
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return '深色模式';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否为深色模式
|
||||||
|
bool isDarkMode(BuildContext context) {
|
||||||
|
switch (state) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
return MediaQuery.of(context).platformBrightness == Brightness.dark;
|
||||||
|
case ThemeMode.light:
|
||||||
|
return false;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 主题模式扩展
|
||||||
|
extension ThemeModeExtension on ThemeMode {
|
||||||
|
/// 获取主题模式名称
|
||||||
|
String get name {
|
||||||
|
switch (this) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
return '跟随系统';
|
||||||
|
case ThemeMode.light:
|
||||||
|
return '浅色模式';
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return '深色模式';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取主题模式图标
|
||||||
|
IconData get icon {
|
||||||
|
switch (this) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
return Icons.brightness_auto;
|
||||||
|
case ThemeMode.light:
|
||||||
|
return Icons.brightness_7;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return Icons.brightness_3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取主题模式描述
|
||||||
|
String get description {
|
||||||
|
switch (this) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
return '根据系统设置自动切换浅色和深色模式';
|
||||||
|
case ThemeMode.light:
|
||||||
|
return '始终使用浅色模式';
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return '始终使用深色模式';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 主题相关工具函数
|
||||||
|
class ThemeUtils {
|
||||||
|
/// 根据系统亮度获取推荐的主题模式
|
||||||
|
static ThemeMode getRecommendedThemeMode(Brightness brightness) {
|
||||||
|
return brightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查当前是否为深色模式
|
||||||
|
static bool isDarkMode(BuildContext context, ThemeMode themeMode) {
|
||||||
|
switch (themeMode) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
return MediaQuery.of(context).platformBrightness == Brightness.dark;
|
||||||
|
case ThemeMode.light:
|
||||||
|
return false;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据索引获取主题模式
|
||||||
|
static ThemeMode getThemeModeByIndex(int index) {
|
||||||
|
return ThemeMode.values[index.clamp(0, ThemeMode.values.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据主题模式获取索引
|
||||||
|
static int getIndexByThemeMode(ThemeMode themeMode) {
|
||||||
|
return ThemeMode.values.indexOf(themeMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
773
pubspec.lock
773
pubspec.lock
@ -1,6 +1,46 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "64.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.0"
|
||||||
|
analyzer_plugin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer_plugin
|
||||||
|
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.3"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.7"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -17,6 +57,94 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
build:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build
|
||||||
|
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
build_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_config
|
||||||
|
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
build_daemon:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_daemon
|
||||||
|
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.1"
|
||||||
|
build_resolvers:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_resolvers
|
||||||
|
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
build_runner:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: build_runner
|
||||||
|
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.9"
|
||||||
|
build_runner_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_runner_core
|
||||||
|
sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "7.3.0"
|
||||||
|
built_collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_collection
|
||||||
|
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
built_value:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_value
|
||||||
|
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "8.12.0"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -25,6 +153,22 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -33,6 +177,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
code_builder:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_builder
|
||||||
|
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.10.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -41,6 +193,30 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3+8"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -49,6 +225,22 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
custom_lint_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: custom_lint_core
|
||||||
|
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.3"
|
||||||
|
dart_style:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_style
|
||||||
|
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.6"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -57,32 +249,274 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.1"
|
||||||
|
flutter_image_compress:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress
|
||||||
|
sha256: "45a3071868092a61b11044c70422b04d39d4d9f2ef536f3c5b11fb65a1e7dd90"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
flutter_image_compress_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_common
|
||||||
|
sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
|
flutter_image_compress_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_macos
|
||||||
|
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
flutter_image_compress_ohos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_ohos
|
||||||
|
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.3"
|
||||||
|
flutter_image_compress_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_platform_interface
|
||||||
|
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
flutter_image_compress_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_web
|
||||||
|
sha256: f02fe352b17f82b72f481de45add240db062a2585850bea1667e82cc4cd6c311
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.4+1"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_launcher_icons
|
||||||
|
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "3.0.2"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_riverpod:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_riverpod
|
||||||
|
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.1"
|
||||||
|
flutter_staggered_grid_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_staggered_grid_view
|
||||||
|
sha256: "1312314293acceb65b92754298754801b0e1f26a1845833b740b30415bbbcf07"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.2"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
freezed_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: freezed_annotation
|
||||||
|
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.4"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
graphs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: graphs
|
||||||
|
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.1"
|
||||||
|
hive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hive
|
||||||
|
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.3"
|
||||||
|
hive_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hive_flutter
|
||||||
|
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
hive_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: hive_generator
|
||||||
|
sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.2"
|
||||||
|
image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.4"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.18.1"
|
||||||
|
io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.1"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "4.9.0"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "3.0.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -99,6 +533,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
material_design_icons_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: material_design_icons_flutter
|
||||||
|
sha256: "6f986b7a51f3ad4c00e33c5c84e8de1bdd140489bbcdc8b66fc1283dad4dea5a"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.7296"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -107,19 +549,235 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
path:
|
mime:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
|
octo_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
path:
|
||||||
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.3"
|
version: "1.8.3"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.4"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
|
photo_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: photo_view
|
||||||
|
sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.14.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
pubspec_parse:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pubspec_parse
|
||||||
|
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
receive_sharing_intent:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: receive_sharing_intent
|
||||||
|
sha256: ec76056e4d258ad708e76d85591d933678625318e411564dcb9059048ca3a593
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.8.1"
|
||||||
|
riverpod:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: riverpod
|
||||||
|
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.1"
|
||||||
|
riverpod_analyzer_utils:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: riverpod_analyzer_utils
|
||||||
|
sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.1"
|
||||||
|
riverpod_annotation:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: riverpod_annotation
|
||||||
|
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.1"
|
||||||
|
riverpod_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: riverpod_generator
|
||||||
|
sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.27.7"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.99"
|
||||||
|
source_gen:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_gen
|
||||||
|
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
|
source_helper:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_helper
|
||||||
|
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.4"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -128,6 +786,22 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.3"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -136,6 +810,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.11.1"
|
||||||
|
state_notifier:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: state_notifier
|
||||||
|
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -144,6 +826,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -152,6 +842,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0+1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -168,6 +866,30 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.6.1"
|
||||||
|
timing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timing
|
||||||
|
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
|
uuid:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -176,6 +898,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -184,5 +914,38 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.3.0"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.5 <4.0.0"
|
dart: ">=3.2.5 <4.0.0"
|
||||||
|
flutter: ">=3.16.6"
|
||||||
|
|||||||
75
pubspec.yaml
75
pubspec.yaml
@ -1,5 +1,5 @@
|
|||||||
name: snap_wish
|
name: insposnap
|
||||||
description: "Shoot What You Want"
|
description: "Shoot What Inspires You - 拍摄灵感收集与管理应用"
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
@ -31,21 +31,67 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
# UI组件和图标
|
||||||
|
cupertino_icons: ^1.0.6
|
||||||
|
material_design_icons_flutter: ^7.0.7296
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# 状态管理
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
flutter_riverpod: ^2.4.9
|
||||||
cupertino_icons: ^1.0.2
|
riverpod_annotation: ^2.3.3
|
||||||
|
|
||||||
|
# 数据存储
|
||||||
|
hive: ^2.2.3
|
||||||
|
hive_flutter: ^1.1.0
|
||||||
|
|
||||||
|
# 图片处理
|
||||||
|
image: ^4.1.7
|
||||||
|
flutter_image_compress: ^2.1.0
|
||||||
|
cached_network_image: ^3.3.1
|
||||||
|
|
||||||
|
# 分享接收
|
||||||
|
receive_sharing_intent: ^1.4.5
|
||||||
|
|
||||||
|
# 国际化
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
intl: ^0.18.1
|
||||||
|
|
||||||
|
# 工具类
|
||||||
|
path: ^1.8.3
|
||||||
|
path_provider: ^2.1.1
|
||||||
|
uuid: ^3.0.7
|
||||||
|
mime: ^1.0.4
|
||||||
|
|
||||||
|
# 图片查看
|
||||||
|
photo_view: ^0.14.0
|
||||||
|
|
||||||
|
# 瀑布流布局
|
||||||
|
flutter_staggered_grid_view: ^0.6.2
|
||||||
|
|
||||||
|
# Flutter启动图标配置
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/icons/app_icon.png"
|
||||||
|
min_sdk_android: 24
|
||||||
|
remove_alpha_ios: true
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# Riverpod代码生成
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
riverpod_generator: ^2.3.9
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
build_runner: ^2.4.7
|
||||||
# package. See that file for information about deactivating specific lint
|
|
||||||
# rules and activating additional ones.
|
# Hive代码生成
|
||||||
flutter_lints: ^2.0.0
|
hive_generator: ^2.0.1
|
||||||
|
|
||||||
|
# 代码质量
|
||||||
|
flutter_lints: ^3.0.1
|
||||||
|
|
||||||
|
# 图标生成
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
@ -59,9 +105,10 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/icons/
|
||||||
# - images/a_dot_ham.jpeg
|
- assets/images/
|
||||||
|
- assets/animations/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|||||||
381
snapwish_PRD_v1.1.md
Normal file
381
snapwish_PRD_v1.1.md
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
# "想拍" App 产品需求文档 (PRD) - MVP版本
|
||||||
|
|
||||||
|
| 文档版本 | V1.1 (MVP) |
|
||||||
|
| ------------ | ----------------------- |
|
||||||
|
| **创建日期** | 2025年9月11日 |
|
||||||
|
| **更新日期** | 2025年9月12日 |
|
||||||
|
| **创建人** | Claude |
|
||||||
|
| **项目名称** | 想拍 (snapwish) |
|
||||||
|
| **项目描述** | Shoot What Inspires You |
|
||||||
|
|
||||||
|
## 1. 引言
|
||||||
|
|
||||||
|
### 1.1. 项目背景
|
||||||
|
|
||||||
|
许多摄影爱好者和创意工作者在日常浏览社交媒体(如微博、Twitter、Instagram、Pinterest等)时,会发现很多能激发自己拍摄灵感的图片。然而,这些图片往往散落在各个平台的收藏夹或手机相册中,难以统一管理和快速查找。当用户想要寻找拍摄灵感时,需要在多个应用和文件夹中翻找,效率低下。"想拍"旨在解决这一痛点,提供一个统一的灵感收集和管理工具。
|
||||||
|
|
||||||
|
### 1.2. 产品愿景
|
||||||
|
|
||||||
|
成为摄影爱好者和创意人士首选的、最便捷的拍摄灵感收集与管理应用。通过无缝的分享体验和强大的分类功能,帮助用户捕捉、整理并随时回顾每一个创意火花。
|
||||||
|
|
||||||
|
### 1.3. 目标用户
|
||||||
|
|
||||||
|
- **摄影爱好者**:希望系统性地收集和整理摄影作品,以供学习和模仿
|
||||||
|
- **设计师/艺术家**:需要收集视觉素材,构建自己的灵感库
|
||||||
|
- **内容创作者/博主**:为自己的内容创作寻找视觉参考和创意灵感
|
||||||
|
- **普通用户**:喜欢美好图片,希望将喜欢的图片收藏并分类管理
|
||||||
|
|
||||||
|
### 1.4. MVP版本范围
|
||||||
|
|
||||||
|
本次V1.1 MVP版本专注于实现核心的灵感收集和管理功能,主要目标是让用户能够通过系统分享功能快速保存图片,并提供基础的文件夹和标签管理功能,以及良好的浏览查看体验。
|
||||||
|
|
||||||
|
## 2. 产品功能详述
|
||||||
|
|
||||||
|
### 2.1. 核心功能:接收并保存分享图片
|
||||||
|
|
||||||
|
#### 2.1.1. 从系统分享菜单接收图片
|
||||||
|
|
||||||
|
**功能描述**:App需要注册为系统分享菜单的目标应用。当用户在任何支持图片分享的应用中选择一张或多张图片并点击"分享"时,可以在分享列表中看到"想拍"App的图标。
|
||||||
|
|
||||||
|
**用户流程**:
|
||||||
|
1. 用户在第三方App中看到一张喜欢的图片
|
||||||
|
2. 用户长按图片或点击分享按钮,唤起系统分享菜单
|
||||||
|
3. 在分享列表中,用户找到并点击"想拍"图标
|
||||||
|
4. 系统显示半透明模态框覆盖在当前App上,展示图片保存界面
|
||||||
|
|
||||||
|
**技术要求**:
|
||||||
|
- 支持接收单张图片和多张图片(最多30张)
|
||||||
|
- 能正确处理传入的图片文件流
|
||||||
|
- 支持最大30MB的图片文件
|
||||||
|
- 支持格式:JPG、PNG、WebP、HEIC、GIF、动态WebP、APNG
|
||||||
|
|
||||||
|
#### 2.1.2. 图片保存界面
|
||||||
|
|
||||||
|
**功能描述**:从分享菜单跳转过来后,展示待保存的图片预览,并提供文件夹和标签选择器,以及备注输入框。
|
||||||
|
|
||||||
|
**界面元素**:
|
||||||
|
- **图片预览区**:清晰展示待保存的图片缩略图。多张图片时显示网格预览,可点击查看大图
|
||||||
|
- **文件夹选择器**:
|
||||||
|
- 点击后弹出文件夹列表供用户选择
|
||||||
|
- 默认显示"未分类"
|
||||||
|
- 列表顶部应有"新建文件夹"的快捷入口
|
||||||
|
- 支持文件夹图标选择
|
||||||
|
- 按最近使用顺序排序
|
||||||
|
- **标签选择器**:
|
||||||
|
- 输入框支持动态匹配已存在的标签,显示3个建议
|
||||||
|
- 用户可以从匹配列表中选择,也可以直接输入新标签
|
||||||
|
- 已选择的标签会显示在输入框下方,支持点击删除
|
||||||
|
- 单张图片标签数量无限制
|
||||||
|
- 单个标签字数限制:20个中文汉字长度
|
||||||
|
- 重复标签自动忽略,更新标签使用顺序
|
||||||
|
- **备注输入框(可选)**:多行文本框,允许用户为图片添加文字描述
|
||||||
|
- **操作模式切换**:批量应用模式(默认) vs 单张编辑模式,切换时保留批量内容到单张编辑
|
||||||
|
- **"保存"按钮**:将图片文件保存到本地,并将元数据存入数据库
|
||||||
|
- **"取消"按钮**:放弃本次保存,关闭该界面
|
||||||
|
|
||||||
|
**保存策略**:
|
||||||
|
- 原图保存至应用专属目录
|
||||||
|
- 自动生成缩略图:长边500px,WebP格式,质量85%,保持原始比例
|
||||||
|
- 按保存日期分类存储:`/yyyy/MM/dd/`
|
||||||
|
- 异步处理,避免阻塞UI
|
||||||
|
- 多张图片保存时显示进度提示
|
||||||
|
|
||||||
|
### 2.2. 组织与管理功能
|
||||||
|
|
||||||
|
#### 2.2.1. 文件夹管理
|
||||||
|
|
||||||
|
**功能描述**:用户可以创建、重命名和删除文件夹。仅支持一级文件夹结构。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **创建**:用户可以在文件夹选择器或专门的文件夹页面创建新文件夹,通过弹窗形式,支持图标选择
|
||||||
|
- **查看**:在"文件夹"标签页中,以网格形式展示所有文件夹及其封面(默认为该文件夹最新一张图)
|
||||||
|
- **重命名**:长按文件夹或通过编辑按钮进行重命名
|
||||||
|
- **删除**:删除文件夹时,其中的图片自动归类到"未分类"文件夹
|
||||||
|
- **排序**:按最近使用顺序排序
|
||||||
|
|
||||||
|
#### 2.2.2. 标签管理
|
||||||
|
|
||||||
|
**功能描述**:用户可以创建和删除标签,并查看所有使用特定标签的图片。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **创建**:在保存图片时输入新标签即可自动创建
|
||||||
|
- **查看**:在"标签"管理页面,以列表形式展示所有标签
|
||||||
|
- **管理**:提供标签编辑功能,支持Material Icons图标,颜色跟随主题
|
||||||
|
- **去重**:按文本内容自动去重
|
||||||
|
- **图标**:支持从Material Icons中选择图标,存储图标名称
|
||||||
|
- **颜色**:跟随应用主题色,存储为十六进制格式
|
||||||
|
|
||||||
|
### 2.3. 浏览与查看功能
|
||||||
|
|
||||||
|
#### 2.3.1. 主图库视图
|
||||||
|
|
||||||
|
**功能描述**:App的主界面,以瀑布流的形式展示所有已保存的图片。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **布局**:瀑布流网格,支持列数切换
|
||||||
|
- 手机(≤600dp):2列
|
||||||
|
- 平板(>600dp):3-4列
|
||||||
|
- **排序**:默认按保存时间倒序排列
|
||||||
|
- **间距**:图片间距8dp
|
||||||
|
- **加载**:支持上拉加载更多,下拉刷新
|
||||||
|
- **空状态**:显示简洁插画和说明文案
|
||||||
|
- **搜索框**:常驻顶部
|
||||||
|
|
||||||
|
#### 2.3.2. 图片详情页
|
||||||
|
|
||||||
|
**功能描述**:点击图库中的任意一张图片,进入详情页。
|
||||||
|
|
||||||
|
**界面元素**:
|
||||||
|
- 高清大图查看,支持手势缩放和拖动
|
||||||
|
- 滑动切换:左右滑动切换上一张/下一张,不支持循环
|
||||||
|
- 双击放大:双击放大/缩小功能
|
||||||
|
- 横竖屏适配:根据设备方向自动调整布局
|
||||||
|
- 显示图片的所属文件夹、所有标签和备注信息
|
||||||
|
- 提供"编辑"按钮,允许用户修改文件夹、标签和备注
|
||||||
|
- 提供"删除"按钮,需要二次确认后删除图片
|
||||||
|
- 提供"导出"按钮,可将图片保存到系统相册
|
||||||
|
|
||||||
|
#### 2.3.3. 搜索功能
|
||||||
|
|
||||||
|
**功能描述**:在主图库顶部提供搜索功能。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **搜索框**:常驻顶部,支持关键词输入
|
||||||
|
- **搜索范围**:文件夹名称、标签名称、备注内容
|
||||||
|
- **触发方式**:点击搜索按钮触发搜索
|
||||||
|
- **搜索历史**:记录最近10条搜索历史
|
||||||
|
- **搜索结果**:在图库中实时展示搜索结果,不高亮匹配文字
|
||||||
|
- **空搜索**:显示缺省页面
|
||||||
|
- **加载优化**:搜索超过1秒时显示loading状态
|
||||||
|
|
||||||
|
### 2.4. 设置功能
|
||||||
|
|
||||||
|
**功能描述**:提供应用的基本设置选项。
|
||||||
|
|
||||||
|
**设置项分组**:
|
||||||
|
- **外观设置**
|
||||||
|
- **语言设置**:简体中文、繁体中文、English
|
||||||
|
- **主题模式**:跟随系统、浅色、深色(保持一定对比度)
|
||||||
|
- **网格布局**:瀑布流(默认)、等宽网格
|
||||||
|
- **存储管理**
|
||||||
|
- **存储使用情况**:显示当前存储占用
|
||||||
|
- **清理缓存**:一键清理缩略图缓存
|
||||||
|
- **关于应用**
|
||||||
|
- **版本信息**:显示当前版本号
|
||||||
|
- **用户协议**:链接到用户协议页面
|
||||||
|
- **隐私政策**:链接到隐私政策页面
|
||||||
|
|
||||||
|
### 2.5. 国际化支持
|
||||||
|
|
||||||
|
**支持语言**:
|
||||||
|
- 简体中文(默认)
|
||||||
|
- 繁体中文
|
||||||
|
- English
|
||||||
|
|
||||||
|
**文案风格**:活泼友好,符合年轻用户喜好
|
||||||
|
|
||||||
|
**App名称**:
|
||||||
|
- 中文:想拍
|
||||||
|
- 英文:snapwish
|
||||||
|
|
||||||
|
## 3. 非功能性需求
|
||||||
|
|
||||||
|
### 3.1. 性能要求
|
||||||
|
|
||||||
|
- **启动速度**:应用冷启动时间应在2秒以内
|
||||||
|
- **加载速度**:图库列表滑动流畅,图片加载不应出现明显卡顿
|
||||||
|
- **响应速度**:所有用户操作的响应时间应在200毫秒内
|
||||||
|
- **内存管理**:合理管理大图片内存,避免OOM
|
||||||
|
- **搜索响应**:搜索响应时间应在500毫秒内
|
||||||
|
|
||||||
|
### 3.2. 易用性要求
|
||||||
|
|
||||||
|
- **UI/UX设计**:界面设计简洁、直观,符合主流操作习惯
|
||||||
|
- **核心操作路径**:分享→选择→保存应尽可能简短
|
||||||
|
- **空状态处理**:各种空状态都有友好的提示和引导
|
||||||
|
- **首次使用**:暂不增加引导,后续版本优化
|
||||||
|
|
||||||
|
### 3.3. 平台要求
|
||||||
|
|
||||||
|
- **技术栈**:使用Flutter框架开发
|
||||||
|
- **目标平台**:Android和iOS
|
||||||
|
- **最低版本**:Android 7.0+,iOS 12.0+
|
||||||
|
- **设备适配**:当前版本暂不考虑特殊设备适配
|
||||||
|
|
||||||
|
### 3.4. 数据存储要求
|
||||||
|
|
||||||
|
- **图片文件**:保存在应用专属的内部存储目录
|
||||||
|
- **缩略图**:单独目录保存,按日期分类存储
|
||||||
|
- **元数据**:使用Hive数据库存储图片信息
|
||||||
|
- **缓存管理**:智能缓存清理,30天/500MB自动清理
|
||||||
|
|
||||||
|
### 3.5. 图片处理要求
|
||||||
|
|
||||||
|
- **原图保存**:保持原始图片质量
|
||||||
|
- **缩略图生成**:长边500px,WebP格式,85%质量,保持原始比例
|
||||||
|
- **异步处理**:图片处理在后台进行,不阻塞UI
|
||||||
|
- **内存优化**:大图片分块加载,避免内存溢出
|
||||||
|
- **大GIF处理**:提取前几帧作为缩略图,避免内存溢出
|
||||||
|
|
||||||
|
## 4. 界面设计规范
|
||||||
|
|
||||||
|
### 4.1. 视觉风格
|
||||||
|
|
||||||
|
- **设计风格**:Material Design
|
||||||
|
- **主题色彩**:支持动态主题色,提供5-6种预设主题色
|
||||||
|
- **图标风格**:Material Icons
|
||||||
|
- **圆角规范**:8dp统一圆角
|
||||||
|
- **深色模式**:保持一定对比度,不完全反转
|
||||||
|
|
||||||
|
### 4.2. 交互规范
|
||||||
|
|
||||||
|
- **手势操作**:单击、长按、滑动、双击、捏合
|
||||||
|
- **动画效果**:Material标准动画,页面转场300ms
|
||||||
|
- **反馈机制**:点击波纹效果、加载状态、操作结果提示
|
||||||
|
- **按钮反馈**:Material标准波纹效果
|
||||||
|
|
||||||
|
### 4.3. 加载状态设计
|
||||||
|
|
||||||
|
- **图片加载**:使用骨架屏,提供良好的加载体验
|
||||||
|
- **保存进度**:多张图片保存时显示进度提示
|
||||||
|
- **搜索加载**:超过1秒时显示loading状态
|
||||||
|
- **列表加载**:下拉刷新和上拉加载更多
|
||||||
|
|
||||||
|
### 4.4. 空状态设计
|
||||||
|
|
||||||
|
- **插画风格**:简洁线性插画
|
||||||
|
- **文案风格**:温暖友好,具有引导性
|
||||||
|
- **操作引导**:提供明确的下一步操作建议
|
||||||
|
- **具体文案**:
|
||||||
|
- 首次使用:"还没有灵感?去其他App分享图片到这里吧~"
|
||||||
|
- 搜索结果为空:"换个关键词试试?或者去收集更多灵感吧!"
|
||||||
|
- 文件夹为空:"这个文件夹还在等它的第一张灵感图片"
|
||||||
|
|
||||||
|
### 4.5. 错误处理
|
||||||
|
|
||||||
|
- **分享失败**:"出了点小问题,请重新分享试试~"
|
||||||
|
- **保存失败**:"出了点小问题,请重新分享试试~"
|
||||||
|
- **存储空间不足**:"出了点小问题,请重新分享试试~"
|
||||||
|
- **网络错误**:当前版本暂不考虑网络相关功能
|
||||||
|
|
||||||
|
## 5. 数据模型规范
|
||||||
|
|
||||||
|
### 5.1. 核心实体
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class InspirationImage {
|
||||||
|
final String id; // UUID
|
||||||
|
final String filePath; // 原图路径
|
||||||
|
final String thumbnailPath; // 缩略图路径
|
||||||
|
final String? folderId; // 文件夹ID
|
||||||
|
final List<String> tags; // 标签ID列表
|
||||||
|
final String? note; // 备注内容
|
||||||
|
final DateTime createdAt; // 创建时间(保存时间)
|
||||||
|
final DateTime updatedAt; // 更新时间
|
||||||
|
final String? originalName; // 原始文件名
|
||||||
|
final int fileSize; // 文件大小
|
||||||
|
final String mimeType; // MIME类型
|
||||||
|
final int? width; // 图片宽度
|
||||||
|
final int? height; // 图片高度
|
||||||
|
final bool isFavorite; // 是否收藏
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageFolder {
|
||||||
|
final String id; // UUID
|
||||||
|
final String name; // 文件夹名称
|
||||||
|
final String? coverImageId; // 封面图片ID
|
||||||
|
final String icon; // Material Icons名称
|
||||||
|
final DateTime createdAt; // 创建时间
|
||||||
|
final DateTime updatedAt; // 更新时间
|
||||||
|
final DateTime lastUsedAt; // 最近使用时间
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageTag {
|
||||||
|
final String id; // UUID
|
||||||
|
final String name; // 标签名称
|
||||||
|
final String icon; // Material Icons名称
|
||||||
|
final String color; // 十六进制颜色
|
||||||
|
final int usageCount; // 使用次数
|
||||||
|
final DateTime lastUsedAt; // 最近使用时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2. 存储路径规范
|
||||||
|
|
||||||
|
```
|
||||||
|
应用专属目录/
|
||||||
|
├── images/ # 原图存储
|
||||||
|
│ └── yyyy/MM/dd/ # 按保存日期分类
|
||||||
|
├── thumbnails/ # 缩略图存储
|
||||||
|
│ └── yyyy/MM/dd/ # 对应原图日期
|
||||||
|
└── cache/ # 缓存目录
|
||||||
|
└── temp/ # 临时文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 开发计划
|
||||||
|
|
||||||
|
### 6.1. MVP版本开发周期(12-17天)
|
||||||
|
|
||||||
|
**Phase 1: 架构搭建(3-4天)**
|
||||||
|
- 项目结构搭建和配置
|
||||||
|
- 主题和国际化基础配置
|
||||||
|
- 数据模型设计和Hive配置
|
||||||
|
- 基础组件封装
|
||||||
|
|
||||||
|
**Phase 2: 分享功能(2-3天)**
|
||||||
|
- 分享接收功能实现
|
||||||
|
- 图片保存界面开发
|
||||||
|
- 文件存储和缩略图生成
|
||||||
|
|
||||||
|
**Phase 3: 图库展示(2-3天)**
|
||||||
|
- 主图库瀑布流界面
|
||||||
|
- 空状态页面设计
|
||||||
|
- 图片加载和缓存优化
|
||||||
|
|
||||||
|
**Phase 4: 管理功能(3-4天)**
|
||||||
|
- 文件夹管理功能
|
||||||
|
- 标签管理系统
|
||||||
|
- 图片详情页开发
|
||||||
|
|
||||||
|
**Phase 5: 搜索设置(2-3天)**
|
||||||
|
- 搜索功能实现
|
||||||
|
- 设置页面开发
|
||||||
|
- 性能优化和bug修复
|
||||||
|
|
||||||
|
### 6.2. 后续版本规划
|
||||||
|
|
||||||
|
**V1.1版本(短期)**
|
||||||
|
- 批量管理功能
|
||||||
|
- 数据备份与恢复
|
||||||
|
- 多种排序方式
|
||||||
|
- 视图切换(列表/网格)
|
||||||
|
|
||||||
|
**V2.0版本(中长期)**
|
||||||
|
- 云同步功能
|
||||||
|
- 智能标签推荐
|
||||||
|
- 图片来源追溯
|
||||||
|
- 主题色提取功能
|
||||||
|
|
||||||
|
## 7. 风险评估
|
||||||
|
|
||||||
|
### 7.1. 技术风险
|
||||||
|
- **大文件处理**:30MB GIF的内存管理需要特别注意,采用帧提取策略
|
||||||
|
- **分享兼容性**:不同Android厂商的分享实现可能存在差异
|
||||||
|
- **存储权限**:Android 13+的媒体权限变更需要适配
|
||||||
|
|
||||||
|
### 7.2. 用户体验风险
|
||||||
|
- **首次使用**:没有引导流程,需要空状态设计足够友好
|
||||||
|
- **性能感知**:大量图片时的流畅度需要重点优化
|
||||||
|
|
||||||
|
### 7.3. 缓解措施
|
||||||
|
- 实现异步图片处理,避免阻塞主线程
|
||||||
|
- 充分测试各种分享场景和设备兼容性
|
||||||
|
- 实现智能缓存和内存管理策略
|
||||||
|
- 采用虚拟滚动和图片懒加载技术
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档状态**:已优化,技术细节已确认
|
||||||
|
**最后更新**:2025年9月12日
|
||||||
|
**确认人员**:产品负责人 + 技术负责人
|
||||||
|
**版本说明**:V1.1版本补充了所有UI/UX细节、技术规范和性能指标
|
||||||
284
snapwish_PRD_v1.md
Normal file
284
snapwish_PRD_v1.md
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
# "想拍" App 产品需求文档 (PRD) - MVP版本
|
||||||
|
|
||||||
|
| 文档版本 | V1.0 (MVP) |
|
||||||
|
| ------------ | ----------------------- |
|
||||||
|
| **创建日期** | 2025年9月11日 |
|
||||||
|
| **创建人** | Claude |
|
||||||
|
| **项目名称** | 想拍 (snapwish) |
|
||||||
|
| **项目描述** | Shoot What Inspires You |
|
||||||
|
|
||||||
|
## 1. 引言
|
||||||
|
|
||||||
|
### 1.1. 项目背景
|
||||||
|
|
||||||
|
许多摄影爱好者和创意工作者在日常浏览社交媒体(如微博、Twitter、Instagram、Pinterest等)时,会发现很多能激发自己拍摄灵感的图片。然而,这些图片往往散落在各个平台的收藏夹或手机相册中,难以统一管理和快速查找。当用户想要寻找拍摄灵感时,需要在多个应用和文件夹中翻找,效率低下。"想拍"旨在解决这一痛点,提供一个统一的灵感收集和管理工具。
|
||||||
|
|
||||||
|
### 1.2. 产品愿景
|
||||||
|
|
||||||
|
成为摄影爱好者和创意人士首选的、最便捷的拍摄灵感收集与管理应用。通过无缝的分享体验和强大的分类功能,帮助用户捕捉、整理并随时回顾每一个创意火花。
|
||||||
|
|
||||||
|
### 1.3. 目标用户
|
||||||
|
|
||||||
|
- **摄影爱好者**:希望系统性地收集和整理摄影作品,以供学习和模仿
|
||||||
|
- **设计师/艺术家**:需要收集视觉素材,构建自己的灵感库
|
||||||
|
- **内容创作者/博主**:为自己的内容创作寻找视觉参考和创意灵感
|
||||||
|
- **普通用户**:喜欢美好图片,希望将喜欢的图片收藏并分类管理
|
||||||
|
|
||||||
|
### 1.4. MVP版本范围
|
||||||
|
|
||||||
|
本次V1.0 MVP版本专注于实现核心的灵感收集和管理功能,主要目标是让用户能够通过系统分享功能快速保存图片,并提供基础的文件夹和标签管理功能,以及良好的浏览查看体验。
|
||||||
|
|
||||||
|
## 2. 产品功能详述
|
||||||
|
|
||||||
|
### 2.1. 核心功能:接收并保存分享图片
|
||||||
|
|
||||||
|
#### 2.1.1. 从系统分享菜单接收图片
|
||||||
|
|
||||||
|
**功能描述**:App需要注册为系统分享菜单的目标应用。当用户在任何支持图片分享的应用中选择一张或多张图片并点击"分享"时,可以在分享列表中看到"想拍"App的图标。
|
||||||
|
|
||||||
|
**用户流程**:
|
||||||
|
1. 用户在第三方App中看到一张喜欢的图片
|
||||||
|
2. 用户长按图片或点击分享按钮,唤起系统分享菜单
|
||||||
|
3. 在分享列表中,用户找到并点击"想拍"图标
|
||||||
|
4. 系统自动跳转至"想拍"App的图片保存界面
|
||||||
|
|
||||||
|
**技术要求**:
|
||||||
|
- 支持接收单张图片和多张图片(最多30张)
|
||||||
|
- 能正确处理传入的图片文件流
|
||||||
|
- 支持最大30MB的图片文件
|
||||||
|
- 支持格式:JPG、PNG、WebP、HEIC、GIF、动态WebP、APNG
|
||||||
|
|
||||||
|
#### 2.1.2. 图片保存界面
|
||||||
|
|
||||||
|
**功能描述**:从分享菜单跳转过来后,展示待保存的图片预览,并提供文件夹和标签选择器,以及备注输入框。
|
||||||
|
|
||||||
|
**界面元素**:
|
||||||
|
- **图片预览区**:清晰展示待保存的图片缩略图。多张图片时显示网格预览,可点击查看大图
|
||||||
|
- **文件夹选择器**:
|
||||||
|
- 点击后弹出文件夹列表供用户选择
|
||||||
|
- 默认显示"未分类"
|
||||||
|
- 列表顶部应有"新建文件夹"的快捷入口
|
||||||
|
- **标签选择器**:
|
||||||
|
- 输入框支持动态匹配已存在的标签
|
||||||
|
- 用户可以从匹配列表中选择,也可以直接输入新标签
|
||||||
|
- 已选择的标签会显示在输入框下方
|
||||||
|
- 单张图片标签数量无限制
|
||||||
|
- 单个标签字数限制:20个中文汉字长度
|
||||||
|
- **备注输入框(可选)**:多行文本框,允许用户为图片添加文字描述
|
||||||
|
- **操作模式切换**:批量应用模式 vs 单张编辑模式
|
||||||
|
- **"保存"按钮**:将图片文件保存到本地,并将元数据存入数据库
|
||||||
|
- **"取消"按钮**:放弃本次保存,关闭该界面
|
||||||
|
|
||||||
|
**保存策略**:
|
||||||
|
- 原图保存至应用专属目录
|
||||||
|
- 自动生成500x500缩略图(WebP格式,质量85%)
|
||||||
|
- 按日期分类存储:`/yyyy/MM/dd/`
|
||||||
|
- 异步处理,避免阻塞UI
|
||||||
|
|
||||||
|
### 2.2. 组织与管理功能
|
||||||
|
|
||||||
|
#### 2.2.1. 文件夹管理
|
||||||
|
|
||||||
|
**功能描述**:用户可以创建、重命名和删除文件夹。仅支持一级文件夹结构。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **创建**:用户可以在文件夹选择器或专门的文件夹页面创建新文件夹
|
||||||
|
- **查看**:在"文件夹"标签页中,以网格形式展示所有文件夹及其封面(默认为该文件夹最新一张图)
|
||||||
|
- **重命名**:长按文件夹或通过编辑按钮进行重命名
|
||||||
|
- **删除**:删除文件夹时,其中的图片自动归类到"未分类"文件夹
|
||||||
|
|
||||||
|
#### 2.2.2. 标签管理
|
||||||
|
|
||||||
|
**功能描述**:用户可以创建和删除标签,并查看所有使用特定标签的图片。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **创建**:在保存图片时输入新标签即可自动创建
|
||||||
|
- **查看**:在"标签"管理页面,以列表形式展示所有标签
|
||||||
|
- **管理**:提供标签编辑功能,支持Material Icons图标,颜色跟随主题
|
||||||
|
- **去重**:按文本内容自动去重
|
||||||
|
- **图标**:支持从Material Icons中选择图标
|
||||||
|
- **颜色**:跟随应用主题色
|
||||||
|
|
||||||
|
### 2.3. 浏览与查看功能
|
||||||
|
|
||||||
|
#### 2.3.1. 主图库视图
|
||||||
|
|
||||||
|
**功能描述**:App的主界面,以瀑布流的形式展示所有已保存的图片。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **布局**:瀑布流网格,手机2列,平板3-4列自适应
|
||||||
|
- **排序**:默认按保存时间倒序排列
|
||||||
|
- **间距**:图片间距8dp
|
||||||
|
- **加载**:支持上拉加载更多,下拉刷新
|
||||||
|
- **空状态**:显示简洁插画和说明文案
|
||||||
|
|
||||||
|
#### 2.3.2. 图片详情页
|
||||||
|
|
||||||
|
**功能描述**:点击图库中的任意一张图片,进入详情页。
|
||||||
|
|
||||||
|
**界面元素**:
|
||||||
|
- 高清大图查看,支持手势缩放和拖动
|
||||||
|
- 滑动切换:左右滑动切换上一张/下一张
|
||||||
|
- 双击放大:双击放大/缩小功能
|
||||||
|
- 横竖屏适配:根据设备方向自动调整布局
|
||||||
|
- 显示图片的所属文件夹、所有标签和备注信息
|
||||||
|
- 提供"编辑"按钮,允许用户修改文件夹、标签和备注
|
||||||
|
- 提供"删除"按钮,确认后删除图片
|
||||||
|
- 提供"导出"按钮,可将图片保存到系统相册
|
||||||
|
|
||||||
|
#### 2.3.3. 搜索功能
|
||||||
|
|
||||||
|
**功能描述**:在主图库顶部提供搜索功能。
|
||||||
|
|
||||||
|
**具体实现**:
|
||||||
|
- **搜索框**:支持关键词输入
|
||||||
|
- **搜索范围**:文件夹名称、标签名称、备注内容
|
||||||
|
- **触发方式**:点击搜索按钮触发搜索
|
||||||
|
- **搜索历史**:记录最近10条搜索历史
|
||||||
|
- **搜索结果**:在图库中实时展示搜索结果
|
||||||
|
|
||||||
|
### 2.4. 设置功能
|
||||||
|
|
||||||
|
**功能描述**:提供应用的基本设置选项。
|
||||||
|
|
||||||
|
**设置项**:
|
||||||
|
- **语言设置**:简体中文、繁体中文、English
|
||||||
|
- **主题模式**:跟随系统、浅色、深色
|
||||||
|
- **网格布局**:瀑布流(默认)、等宽网格
|
||||||
|
- **存储管理**:显示存储使用情况,清理缓存
|
||||||
|
- **关于**:版本信息、用户协议、隐私政策
|
||||||
|
|
||||||
|
### 2.5. 国际化支持
|
||||||
|
|
||||||
|
**支持语言**:
|
||||||
|
- 简体中文(默认)
|
||||||
|
- 繁体中文
|
||||||
|
- English
|
||||||
|
|
||||||
|
**文案风格**:活泼友好,符合年轻用户喜好
|
||||||
|
|
||||||
|
**App名称**:
|
||||||
|
- 中文:想拍
|
||||||
|
- 英文:snapwish
|
||||||
|
|
||||||
|
## 3. 非功能性需求
|
||||||
|
|
||||||
|
### 3.1. 性能要求
|
||||||
|
|
||||||
|
- **启动速度**:应用冷启动时间应在3秒以内
|
||||||
|
- **加载速度**:图库列表滑动流畅,图片加载不应出现明显卡顿
|
||||||
|
- **响应速度**:所有用户操作的响应时间应在200毫秒内
|
||||||
|
- **内存管理**:合理管理大图片内存,避免OOM
|
||||||
|
|
||||||
|
### 3.2. 易用性要求
|
||||||
|
|
||||||
|
- **UI/UX设计**:界面设计简洁、直观,符合主流操作习惯
|
||||||
|
- **核心操作路径**:分享→选择→保存应尽可能简短
|
||||||
|
- **空状态处理**:各种空状态都有友好的提示和引导
|
||||||
|
|
||||||
|
### 3.3. 平台要求
|
||||||
|
|
||||||
|
- **技术栈**:使用Flutter框架开发
|
||||||
|
- **目标平台**:Android和iOS
|
||||||
|
- **最低版本**:Android 7.0+,iOS 12.0+
|
||||||
|
|
||||||
|
### 3.4. 数据存储要求
|
||||||
|
|
||||||
|
- **图片文件**:保存在应用专属的内部存储目录
|
||||||
|
- **缩略图**:单独目录保存,定期清理策略
|
||||||
|
- **元数据**:使用Hive数据库存储图片信息
|
||||||
|
- **缓存管理**:智能缓存清理,避免占用过多空间
|
||||||
|
|
||||||
|
### 3.5. 图片处理要求
|
||||||
|
|
||||||
|
- **原图保存**:保持原始图片质量
|
||||||
|
- **缩略图生成**:500x500像素,WebP格式,85%质量
|
||||||
|
- **异步处理**:图片处理在后台进行,不阻塞UI
|
||||||
|
- **内存优化**:大图片分块加载,避免内存溢出
|
||||||
|
|
||||||
|
## 4. 界面设计规范
|
||||||
|
|
||||||
|
### 4.1. 视觉风格
|
||||||
|
|
||||||
|
- **设计风格**:Material Design
|
||||||
|
- **主题色彩**:支持动态主题色,跟随系统或用户选择
|
||||||
|
- **图标风格**:Material Icons
|
||||||
|
- **圆角规范**:8dp统一圆角
|
||||||
|
|
||||||
|
### 4.2. 交互规范
|
||||||
|
|
||||||
|
- **手势操作**:单击、长按、滑动、双击、捏合
|
||||||
|
- **动画效果**:页面转场、元素出现/消失、状态变化
|
||||||
|
- **反馈机制**:点击反馈、加载状态、操作结果提示
|
||||||
|
|
||||||
|
### 4.3. 空状态设计
|
||||||
|
|
||||||
|
- **插画风格**:简洁线性插画
|
||||||
|
- **文案风格**:温暖友好,具有引导性
|
||||||
|
- **操作引导**:提供明确的下一步操作建议
|
||||||
|
|
||||||
|
## 5. 开发计划
|
||||||
|
|
||||||
|
### 5.1. MVP版本开发周期(12-17天)
|
||||||
|
|
||||||
|
**Phase 1: 架构搭建(3-4天)**
|
||||||
|
- 项目结构搭建和配置
|
||||||
|
- 主题和国际化基础配置
|
||||||
|
- 数据模型设计和Hive配置
|
||||||
|
- 基础组件封装
|
||||||
|
|
||||||
|
**Phase 2: 分享功能(2-3天)**
|
||||||
|
- 分享接收功能实现
|
||||||
|
- 图片保存界面开发
|
||||||
|
- 文件存储和缩略图生成
|
||||||
|
|
||||||
|
**Phase 3: 图库展示(2-3天)**
|
||||||
|
- 主图库瀑布流界面
|
||||||
|
- 空状态页面设计
|
||||||
|
- 图片加载和缓存优化
|
||||||
|
|
||||||
|
**Phase 4: 管理功能(3-4天)**
|
||||||
|
- 文件夹管理功能
|
||||||
|
- 标签管理系统
|
||||||
|
- 图片详情页开发
|
||||||
|
|
||||||
|
**Phase 5: 搜索设置(2-3天)**
|
||||||
|
- 搜索功能实现
|
||||||
|
- 设置页面开发
|
||||||
|
- 性能优化和bug修复
|
||||||
|
|
||||||
|
### 5.2. 后续版本规划
|
||||||
|
|
||||||
|
**V1.1版本(短期)**
|
||||||
|
- 批量管理功能
|
||||||
|
- 数据备份与恢复
|
||||||
|
- 多种排序方式
|
||||||
|
- 视图切换(列表/网格)
|
||||||
|
|
||||||
|
**V2.0版本(中长期)**
|
||||||
|
- 云同步功能
|
||||||
|
- 智能标签推荐
|
||||||
|
- 图片来源追溯
|
||||||
|
- 主题色提取功能
|
||||||
|
|
||||||
|
## 6. 风险评估
|
||||||
|
|
||||||
|
### 6.1. 技术风险
|
||||||
|
- **大文件处理**:30MB GIF的内存管理需要特别注意
|
||||||
|
- **分享兼容性**:不同Android厂商的分享实现可能存在差异
|
||||||
|
- **存储权限**:Android 13+的媒体权限变更需要适配
|
||||||
|
|
||||||
|
### 6.2. 用户体验风险
|
||||||
|
- **首次使用**:没有引导流程,需要空状态设计足够友好
|
||||||
|
- **性能感知**:大量图片时的流畅度需要重点优化
|
||||||
|
|
||||||
|
### 6.3. 缓解措施
|
||||||
|
- 实现异步图片处理,避免阻塞主线程
|
||||||
|
- 充分测试各种分享场景和设备兼容性
|
||||||
|
- 实现智能缓存和内存管理策略
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档状态**:已确认,进入开发阶段
|
||||||
|
**最后更新**:2025年9月11日
|
||||||
|
**确认人员**:产品负责人 + 技术负责人
|
||||||
@ -1,30 +0,0 @@
|
|||||||
// 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:snap_wish/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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user