Compare commits
2 Commits
6926c78bef
...
34e91fbd70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34e91fbd70 | ||
|
|
2b956eb96c |
@ -2,7 +2,10 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(flutter analyze:*)",
|
||||
"Bash(flutter run:*)"
|
||||
"Bash(flutter run:*)",
|
||||
"Bash(tree:*)",
|
||||
"Bash(flutter pub get:*)",
|
||||
"Bash(sed:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
391
CLAUDE.md
391
CLAUDE.md
@ -1,122 +1,339 @@
|
||||
# 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` - 包含根组件和导航设置
|
||||
- **导航**: 底部导航栏包含 2 个标签页(照片、分类)
|
||||
- **悬浮按钮**: 通用添加照片按钮
|
||||
- **页面**: 位于 `lib/pages/` 目录
|
||||
### 核心技术选型
|
||||
- **框架**:Flutter 3.2.5+ (Dart)
|
||||
- **状态管理**:Riverpod (现代、类型安全)
|
||||
- **数据库**:Hive (轻量级、高性能)
|
||||
- **图片处理**:flutter_image_compress + image
|
||||
- **瀑布流**:flutter_staggered_grid_view
|
||||
- **国际化**:flutter_localizations
|
||||
- **分享接收**:receive_sharing_intent
|
||||
- **图片查看**:photo_view
|
||||
- **UI组件**:Material Design + 自定义主题
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 主结构 (`lib/main.dart`)
|
||||
- **SnapWishApp**: 根 MaterialApp 组件,包含主题配置
|
||||
- **MainPage**: 管理底部导航的有状态组件
|
||||
- **路由**: `/add-photo` 路由用于添加新照片
|
||||
|
||||
### 页面 (`lib/pages/`)
|
||||
- **PhotoPage** (`photo_page.dart`): 照片网格显示和照片详情查看器
|
||||
- **CategoriesPage** (`categories_page.dart`): 文件夹/分类管理,包含创建/删除功能
|
||||
- **AddPhotoPage** (`add_photo_page.dart`): 照片上传表单,包含分类选择和标签功能
|
||||
### 架构设计
|
||||
```
|
||||
lib/
|
||||
├── core/ # 核心功能
|
||||
│ ├── constants/ # 常量定义
|
||||
│ ├── errors/ # 错误处理
|
||||
│ ├── utils/ # 工具类(图片压缩、文件操作)
|
||||
│ └── theme/ # 主题配置
|
||||
├── data/ # 数据层
|
||||
│ ├── datasources/ # 数据源
|
||||
│ │ ├── local/ # Hive数据库
|
||||
│ │ └── share/ # 分享接收
|
||||
│ ├── models/ # 数据模型
|
||||
│ └── repositories/ # 数据仓库
|
||||
├── domain/ # 业务逻辑层
|
||||
│ ├── entities/ # 实体类
|
||||
│ ├── repositories/ # 仓库接口
|
||||
│ └── usecases/ # 用例
|
||||
├── presentation/ # 表示层
|
||||
│ ├── providers/ # Riverpod状态管理
|
||||
│ ├── pages/ # 页面
|
||||
│ ├── widgets/ # 组件
|
||||
│ └── l10n/ # 国际化
|
||||
└── main.dart # 应用入口
|
||||
```
|
||||
|
||||
## 开发命令
|
||||
|
||||
### 构建与运行
|
||||
```bash
|
||||
# 安装依赖
|
||||
flutter pub get
|
||||
|
||||
# 运行开发服务器
|
||||
flutter run
|
||||
|
||||
# 构建 APK
|
||||
flutter build apk
|
||||
|
||||
# 构建 iOS 应用
|
||||
flutter build ios
|
||||
|
||||
# 构建 Web 应用
|
||||
flutter build web
|
||||
```
|
||||
|
||||
### 测试与代码质量
|
||||
```bash
|
||||
# 运行所有测试
|
||||
flutter test
|
||||
|
||||
# 运行特定测试文件
|
||||
flutter test test/widget_test.dart
|
||||
# 运行应用
|
||||
flutter run # 默认设备
|
||||
flutter run -d android # Android设备
|
||||
flutter run -d ios # iOS设备
|
||||
|
||||
# 代码分析
|
||||
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** 进行本地状态管理
|
||||
- 使用 **setState** 进行 UI 更新
|
||||
- 目前使用模拟数据(标有 TODO 注释)
|
||||
### 代码规范
|
||||
- 遵循Dart官方代码风格指南
|
||||
- 使用有意义的变量和函数命名
|
||||
- 添加必要的代码注释
|
||||
- 保持widget的纯净性,业务逻辑分离到provider
|
||||
|
||||
### 导航
|
||||
- **命名路由** 用于主要导航 (`/add-photo`)
|
||||
- **Navigator.push** 用于照片详情查看
|
||||
- **BottomNavigationBar** 用于主标签页切换
|
||||
### 性能要求
|
||||
- 冷启动时间 < 2秒
|
||||
- 图片加载流畅,无卡顿
|
||||
- 用户操作响应 < 200ms
|
||||
- 内存占用 < 200MB
|
||||
|
||||
### UI 组件
|
||||
- **Material 3** 设计系统 (`useMaterial3: true`)
|
||||
- **中文界面**(标签为中文)
|
||||
- **响应式网格布局** 用于照片显示
|
||||
- **对话框式** 交互用于分类管理
|
||||
### 错误处理
|
||||
- 分享失败:"出了点小问题,请重新分享试试~"
|
||||
- 保存失败:同上
|
||||
- 存储空间不足:同上
|
||||
- 所有错误都需要友好提示
|
||||
|
||||
### 数据结构模式
|
||||
- **模拟数据** 目前以 Lists/Maps 实现
|
||||
- **分类结构**: `{name: String, count: int, isDefault: bool}`
|
||||
- **标签管理**: 字符串列表支持 # 前缀
|
||||
### 兼容性要求
|
||||
- Android 7.0+ (API 24+)
|
||||
- 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);
|
||||
}
|
||||
}
|
||||
101
lib/main.dart
101
lib/main.dart
@ -1,74 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'pages/photo_page.dart';
|
||||
import 'pages/categories_page.dart';
|
||||
import 'pages/add_photo_page.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'core/constants/app_constants.dart';
|
||||
import 'core/utils/logger.dart';
|
||||
import 'presentation/app_widget.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const SnapWishApp());
|
||||
/// 应用程序主入口
|
||||
/// 负责初始化应用并配置全局设置
|
||||
void main() async {
|
||||
// 确保Flutter绑定已初始化
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 初始化日志系统
|
||||
Logger.setLogLevel(LogLevel.info);
|
||||
Logger.info('应用启动 - ${AppConstants.appName} v${AppConstants.appVersion}');
|
||||
|
||||
// 捕获并记录未处理的异常
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
Logger.logException('Flutter Error', details.exception, stackTrace: details.stack);
|
||||
FlutterError.presentError(details);
|
||||
};
|
||||
|
||||
// 运行应用
|
||||
runApp(
|
||||
const ProviderScope(
|
||||
child: AppWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class SnapWishApp extends StatelessWidget {
|
||||
const SnapWishApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'SnapWish',
|
||||
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});
|
||||
|
||||
@override
|
||||
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
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -17,6 +57,94 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -25,6 +153,22 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -33,6 +177,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -41,6 +193,30 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -49,6 +225,22 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -57,32 +249,274 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -99,6 +533,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -107,19 +549,235 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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
|
||||
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:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -128,6 +786,22 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -136,6 +810,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -144,6 +826,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -152,6 +842,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -168,6 +866,30 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -176,6 +898,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -184,5 +914,38 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dart: ">=3.2.5 <4.0.0"
|
||||
flutter: ">=3.16.6"
|
||||
|
||||
79
pubspec.yaml
79
pubspec.yaml
@ -1,5 +1,5 @@
|
||||
name: snap_wish
|
||||
description: "Shoot What You Want"
|
||||
name: insposnap
|
||||
description: "Shoot What Inspires You - 拍摄灵感收集与管理应用"
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# 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
|
||||
@ -30,22 +30,68 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# UI组件和图标
|
||||
cupertino_icons: ^1.0.6
|
||||
material_design_icons_flutter: ^7.0.7296
|
||||
|
||||
# 状态管理
|
||||
flutter_riverpod: ^2.4.9
|
||||
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
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.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:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
# Riverpod代码生成
|
||||
riverpod_generator: ^2.3.9
|
||||
build_runner: ^2.4.7
|
||||
|
||||
# Hive代码生成
|
||||
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
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@ -59,9 +105,10 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/icons/
|
||||
- assets/images/
|
||||
- assets/animations/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# 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