主要变更: 1. 新增多风格音效资源与管理文档 2. 修复翻译服务空响应处理与Dio日志异常捕获 3. 完善Web端平台适配与路径获取Stub 4. 优化设备配对与文件传输功能 5. 新增角色命名常量与摇一摇检测器 6. 修复Riverpod dispose与鸿蒙导航路由 7. 新增每日通知服务与流体着色器 8. 优化备份服务与数据管理页面 9. 新增隐私设置附近设备发现选项 10. 重构诗词提供者支持历史记录 11. 完善桌面端构建配置与开发脚本 12. 清理旧版工具部署脚本
38 KiB
字体管理页面重构与功能增强 实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 重构字体管理页面,修复核心缺陷(自定义字体全局生效/序列化脆弱/鸿蒙端不可用),并增强交互体验(预览大图/滑动删除/搜索筛选/动画/收藏等),使字体管理达到 iOS 26 风格的精致水准。
Architecture: 基于 Riverpod 状态管理 + AppThemeExtension 动态主题 + 项目统一组件库(AppSlidable/AppBottomSheet/SkeletonBox/AppIcon)。数据层使用 Hive(AppKVStore) + JSON 序列化替代脆弱的 | 分隔符。鸿蒙端通过条件导入 + path_provider_ohos + file_picker_ohos 实现适配。
Tech Stack: Flutter 3.27+ / Riverpod 3.x / Hive / Supabase(在线字体数据) / google_fonts / flutter_slidable / stupid_simple_sheet / flutter_animate / heroine / shimmer / custom_refresh_indicator / archive / share_plus / pinyin / flutter_svg
文件结构
新建文件
| 文件 | 职责 |
|---|---|
lib/features/settings/presentation/font/font_preview_sheet.dart |
字体预览大图 Sheet(半屏面板+自定义预览文本+字号滑块) |
lib/features/settings/presentation/font/font_search_bar.dart |
字体搜索栏组件(搜索+筛选+拼音匹配) |
lib/features/settings/presentation/font/font_comparison_page.dart |
字体对比模式页面(左右分屏对比两种字体) |
lib/features/settings/services/font_sync_service.dart |
在线字体数据同步服务(Supabase 远程字体列表) |
assets/svgs/font_import.svg |
字体导入图标 |
assets/svgs/font_url.svg |
URL下载图标 |
assets/svgs/font_online.svg |
在线字体图标 |
assets/svgs/font_preview.svg |
字体预览图标 |
assets/svgs/font_compare.svg |
字体对比图标 |
assets/svgs/font_favorite.svg |
字体收藏图标 |
assets/svgs/font_zip.svg |
ZIP导入图标 |
修改文件
| 文件 | 修改内容 |
|---|---|
lib/features/settings/presentation/font_models.dart |
FontInfo 增加 author/license/category/tags/isFavorite/installedAt/sourceUrl/thumbnailUrl 字段 + toJson/fromJson + FontManagementState 增加 searchQuery/filterCategory/sortBy/downloadQueue/error 字段 |
lib/features/settings/presentation/font_management_notifier.dart |
修复 _applyCustomFont / JSON序列化 / 鸿蒙端适配 / 并发下载控制 / 下载重试 / 内置字体映射动态化 / 批量删除 / ZIP导入 / 收藏持久化 |
lib/features/settings/presentation/font_widgets.dart |
AppSlidable滑动删除 / 预览大图Sheet / 搜索栏 / 骨架屏 / 入场动画 / Hero动画 / 下拉刷新 / 下载进度圆环 / emoji→icon替换 / ScrollController滚动定位 |
lib/features/settings/presentation/font_management_page.dart |
改为 StatefulWidget + ScrollController + 下拉刷新 + 搜索栏集成 |
lib/features/settings/providers/theme_settings_provider.dart |
ThemeSettingsState 增加 customFontFamily 字段 + setCustomFontFamily 方法 |
lib/core/theme/app_theme.dart |
buildFromSettings 使用 customFontFamily |
lib/app/app.dart |
effectiveFontFamily 逻辑适配 customFontFamily |
lib/shared/widgets/app_icon.dart |
AppIconData 增加字体相关 SVG 常量 |
pubspec.yaml |
assets/svgs/ 下新增 SVG 文件声明(如需) |
CHANGELOG.md |
版本记录 |
Phase 1: 核心缺陷修复(必须先完成)
Task 1: 修复 _applyCustomFont — 自定义字体全局生效
Files:
-
Modify:
lib/features/settings/providers/theme_settings_provider.dart -
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Modify:
lib/app/app.dart -
Step 1: ThemeSettingsState 增加 customFontFamily 字段
在 theme_settings_provider.dart 的 ThemeSettingsState 中:
class ThemeSettingsState {
const ThemeSettingsState({
// ... 现有字段
this.customFontFamily = '', // 空字符串表示无自定义字体
});
final String customFontFamily;
// ... copyWith 中也增加 customFontFamily
}
在 ThemeSettingsNotifier 中增加方法:
void setCustomFontFamily(String fontFamily) {
state = state.copyWith(customFontFamily: fontFamily);
AppKVStore.setString('${_keyPrefix}custom_font_family', fontFamily);
}
在 _initState() 加载中增加:
final customFontFamily = AppKVStore.getString('${_keyPrefix}custom_font_family') ?? '';
- Step 2: 修复 font_management_notifier.dart 中的 _applyCustomFont
void _applyCustomFont(String fontFamily) {
ref.read(themeSettingsProvider.notifier).setCustomFontFamily(fontFamily);
Log.i('自定义字体已应用: $fontFamily (通过 customFontFamily 传递给主题系统)');
}
同时修复 setActiveFont 中内置字体切换时清除 customFontFamily:
void setActiveFont(String fontFamily) async {
// ...
if (isBuiltIn) {
ref.read(themeSettingsProvider.notifier).setCustomFontFamily('');
final fontStyle = _builtInFonts.firstWhere(
(f) => f.fontFamily == fontFamily,
orElse: () => const FontInfo(name: '', fontFamily: 'Inter', path: '', isBuiltIn: true),
);
final id = _builtInFontStyleMap[fontStyle.fontFamily] ?? 'system';
ref.read(themeSettingsProvider.notifier).setFontStyle(id);
} else {
// ... 加载字体到引擎
_applyCustomFont(fontFamily);
}
}
- Step 3: 修改 app.dart 中的 effectiveFontFamily 逻辑
final effectiveFontFamily = settings.customFontFamily.isNotEmpty
? settings.customFontFamily
: (!builtInFontFamilies.contains(fontState.activeFontFamily)
? fontState.activeFontFamily
: settings.fontStyle.fontFamily);
- Step 4: 运行分析验证
Run: .\scripts\analyze.ps1
Expected: No issues found
- Step 5: Commit
git add -A && git commit -m "fix: 自定义字体通过 customFontFamily 传递给全局主题系统,确保全局生效"
Task 2: 序列化改用 JSON — 修复脆弱的 | 分隔符
Files:
-
Modify:
lib/features/settings/presentation/font_models.dart -
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Step 1: FontInfo 增加 toJson/fromJson
在 font_models.dart 的 FontInfo 中:
Map<String, dynamic> toJson() => {
'name': name,
'fontFamily': fontFamily,
'path': path,
'isBuiltIn': isBuiltIn,
'isDownloaded': isDownloaded,
'fileSize': fileSize,
'author': author,
'license': license,
'category': category,
'isFavorite': isFavorite,
'installedAt': installedAt?.millisecondsSinceEpoch,
'sourceUrl': sourceUrl,
};
factory FontInfo.fromJson(Map<String, dynamic> json) => FontInfo(
name: json['name'] as String? ?? '',
fontFamily: json['fontFamily'] as String? ?? '',
path: json['path'] as String? ?? '',
isBuiltIn: json['isBuiltIn'] as bool? ?? false,
isDownloaded: json['isDownloaded'] as bool? ?? false,
fileSize: json['fileSize'] as int?,
author: json['author'] as String?,
license: json['license'] as String?,
category: json['category'] as String?,
isFavorite: json['isFavorite'] as bool? ?? false,
installedAt: json['installedAt'] != null
? DateTime.fromMillisecondsSinceEpoch(json['installedAt'] as int)
: null,
sourceUrl: json['sourceUrl'] as String?,
);
- Step 2: 修改 Notifier 中的序列化/反序列化
List<FontInfo> _loadInstalledFontsFromKV() {
final raw = AppKVStore.getString(_kvKeyInstalledFonts);
if (raw == null || raw.isEmpty) return [];
try {
final list = jsonDecode(raw) as List;
return list.map((e) => FontInfo.fromJson(e as Map<String, dynamic>)).toList();
} catch (e) {
Log.e('字体列表解析失败,尝试旧格式迁移', e);
return _migrateFromOldFormat();
}
}
void _saveInstalledFontsToKV(List<FontInfo> fonts) {
final json = jsonEncode(fonts.map((f) => f.toJson()).toList());
AppKVStore.setString(_kvKeyInstalledFonts, json);
}
List<FontInfo> _migrateFromOldFormat() {
final raw = AppKVStore.getStringList(_kvKeyInstalledFonts) ?? [];
final fonts = raw.map((entry) {
final parts = entry.split('|');
if (parts.length >= 3) {
return FontInfo(
name: parts[0], fontFamily: parts[1], path: parts[2],
isDownloaded: true, fileSize: parts.length >= 4 ? int.tryParse(parts[3]) : null,
);
}
return null;
}).whereType<FontInfo>().toList();
if (fonts.isNotEmpty) _saveInstalledFontsToKV(fonts);
return fonts;
}
- Step 3: 运行分析验证
Run: .\scripts\analyze.ps1
Expected: No issues found
- Step 4: Commit
git add -A && git commit -m "fix: 字体序列化改用JSON,兼容旧|分隔符格式自动迁移"
Task 3: 内置字体映射动态化
Files:
-
Modify:
lib/features/settings/presentation/font_models.dart -
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Step 1: 在 font_models.dart 中定义内置字体配置
class BuiltInFontConfig {
const BuiltInFontConfig({
required this.name,
required this.fontFamily,
required this.styleId,
required this.icon,
});
final String name;
final String fontFamily;
final String styleId;
final IconData icon;
}
const builtInFontConfigs = [
BuiltInFontConfig(name: '系统默认', fontFamily: 'Inter', styleId: 'system', icon: CupertinoIcons.device_phone_portrait),
BuiltInFontConfig(name: '衬线体', fontFamily: 'NotoSerif', styleId: 'serif', icon: CupertinoIcons.book_fill),
BuiltInFontConfig(name: '等宽体', fontFamily: 'RobotoMono', styleId: 'mono', icon: CupertinoIcons.keyboard),
BuiltInFontConfig(name: '圆体', fontFamily: 'Nunito', styleId: 'rounded', icon: CupertinoIcons.circle_fill),
];
- Step 2: Notifier 中使用动态映射替代硬编码 idMap
String? _getStyleIdForFontFamily(String fontFamily) {
final config = builtInFontConfigs.where((c) => c.fontFamily == fontFamily).firstOrNull;
return config?.styleId;
}
在 setActiveFont 中:
final styleId = _getStyleIdForFontFamily(fontFamily);
if (styleId != null) {
ref.read(themeSettingsProvider.notifier).setCustomFontFamily('');
ref.read(themeSettingsProvider.notifier).setFontStyle(styleId);
}
- Step 3: 运行分析验证并 Commit
git add -A && git commit -m "refactor: 内置字体映射动态化,消除硬编码idMap"
Task 4: 鸿蒙端字体功能适配
Files:
-
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Step 1: 分析鸿蒙端限制并制定适配策略
根据项目文档:
-
path_provider_ohos已适配 ✅ →getApplicationDocumentsDirectory()可用 -
file_picker鸿蒙端已适配(版本 8.0.6)✅ → 文件选择可用 -
dio纯 Dart ✅ → HTTP 下载可用 -
FontLoader是 Flutter 引擎功能 → 鸿蒙端可用 ✅ -
唯一限制:鸿蒙端文件系统路径可能不同,需要验证
-
Step 2: 移除 isOhos 的 return 限制
在 font_management_notifier.dart 中,移除所有 if (pu.isOhos) { AppToast.showInfo('鸿蒙端暂不支持...'); return; } 代码块:
downloadFont()— 移除 isOhos 检查importFont()— 移除 isOhos 检查downloadFontFromUrl()— 移除 isOhos 检查
保留 if (pu.isWeb) 的检查(Web 端确实不支持本地文件操作)。
- Step 3: 增加鸿蒙端路径适配
Future<Directory> _getFontDirectory() async {
if (pu.isWeb) throw UnsupportedError('Web端不支持字体管理');
final dir = await getApplicationDocumentsDirectory();
final fontDir = Directory('${dir.path}/fonts');
if (!await fontDir.exists()) {
await fontDir.create(recursive: true);
}
return fontDir;
}
将所有 getApplicationDocumentsDirectory() + fonts 子目录的操作统一使用 _getFontDirectory()。
- Step 4: 运行分析验证并 Commit
git add -A && git commit -m "feat: 鸿蒙端字体功能适配,移除isOhos限制,统一字体目录获取"
Phase 2: 交互体验增强
Task 5: 字体预览大图 Sheet
Files:
-
Create:
lib/features/settings/presentation/font/font_preview_sheet.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: 创建 FontPreviewSheet 组件
使用 AppBottomSheet.showHalf 实现半屏预览面板:
class FontPreviewSheet extends ConsumerStatefulWidget {
const FontPreviewSheet({super.key, required this.font});
final FontInfo font;
static Future<void> show(BuildContext context, FontInfo font) {
return AppBottomSheet.showHalf(
context: context,
builder: (_) => FontPreviewSheet(font: font),
);
}
}
class _FontPreviewSheetState extends ConsumerState<FontPreviewSheet> {
double _fontSize = 20.0;
final _previewController = TextEditingController(text: '闲言 AaBbCc 你好世界 0123456789');
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return Column(
children: [
// 标题栏: 字体名 + 关闭按钮
// 自定义预览文本输入框
CupertinoTextField(controller: _previewController, ...)
// 字号滑块
CupertinoSlider(value: _fontSize, min: 12, max: 48, onChanged: ...)
// 预览区域
Expanded(child: ListView(children: [
// 中文预览
Text(_previewController.text, style: TextStyle(fontFamily: widget.font.fontFamily, fontSize: _fontSize)),
// 英文预览
Text('The quick brown fox...', style: TextStyle(fontFamily: widget.font.fontFamily, fontSize: _fontSize * 0.8)),
// 数字预览
Text('0123456789', style: TextStyle(fontFamily: widget.font.fontFamily, fontSize: _fontSize * 0.8)),
// 字体信息
_buildFontInfo(ext),
])),
],
);
}
}
- Step 2: 在 FontItemWidget 和 FontOnlineItemWidget 中添加预览入口
点击字体项时弹出预览 Sheet(长按或 info 图标):
GestureDetector(
onTap: onActivate,
onLongPress: () => FontPreviewSheet.show(context, font),
child: ...
)
- Step 3: 运行分析验证并 Commit
git add -A && git commit -m "feat: 字体预览大图Sheet(半屏面板+自定义预览文本+字号滑块)"
Task 6: 字体卡片滑动删除
Files:
-
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: FontItemWidget 外层包裹 AppSlidable
参考项目中 note_list_page.dart 的用法:
AppSlidable(
slideKey: ValueKey(font.fontFamily),
groupTag: 'font_items',
rightActions: [
if (!font.isBuiltIn)
SlideActionConfig(
type: SlideActionType.delete,
onPressed: () async {
final confirmed = await AppSlidableDeleteConfirm.show(
context,
title: '删除 ${font.name}',
message: '删除后无法恢复,确定要删除吗?',
);
if (confirmed) onDelete?.call();
},
),
SlideActionConfig(
type: SlideActionType.favorite,
onPressed: () => _toggleFavorite(ref, font),
),
],
child: /* 原有 FontItemWidget 内容 */,
)
- Step 2: 移除原有删除按钮(Icon CupertinoIcons.delete)
滑动删除替代了原有的删除按钮,非激活态不再显示删除图标。
- Step 3: 运行分析验证并 Commit
git add -A && git commit -m "feat: 字体卡片滑动删除+收藏(AppSlidable),替代原有删除按钮"
Task 7: 搜索/筛选功能
Files:
-
Create:
lib/features/settings/presentation/font/font_search_bar.dart -
Modify:
lib/features/settings/presentation/font_models.dart -
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: FontManagementState 增加搜索/筛选字段
class FontManagementState {
const FontManagementState({
// ... 现有字段
this.searchQuery = '',
this.filterCategory = 'all',
});
final String searchQuery;
final String filterCategory;
List<FontInfo> get filteredFonts {
var result = fonts;
if (searchQuery.isNotEmpty) {
final query = searchQuery.toLowerCase();
result = result.where((f) {
final pinyinMatch = PinyinHelper.getShortPinyin(f.name).toLowerCase().contains(query);
return f.name.toLowerCase().contains(query) ||
f.fontFamily.toLowerCase().contains(query) ||
pinyinMatch;
}).toList();
}
if (filterCategory != 'all') {
result = result.where((f) => f.category == filterCategory).toList();
}
return result;
}
}
- Step 2: Notifier 增加搜索/筛选方法
void setSearchQuery(String query) {
state = state.copyWith(searchQuery: query);
}
void setFilterCategory(String category) {
state = state.copyWith(filterCategory: category);
}
- Step 3: 创建 FontSearchBar 组件
class FontSearchBar extends ConsumerWidget {
const FontSearchBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ext = AppTheme.ext(context);
return Row(
children: [
Expanded(
child: CupertinoSearchTextField(
onChanged: (v) => ref.read(fontManagementProvider.notifier).setSearchQuery(v),
placeholder: '搜索字体(支持拼音)',
style: AppTypography.subhead.copyWith(color: ext.textPrimary),
),
),
CupertinoButton(
padding: const EdgeInsets.only(left: AppSpacing.xs),
onPressed: () => _showFilterSheet(context, ref),
child: Icon(CupertinoIcons.slider_horizontal_3, color: ext.accent),
),
],
);
}
}
- Step 4: FontLocalSection 使用 filteredFonts 替代 fonts
final filteredFonts = state.filteredFonts;
// ... 使用 filteredFonts 渲染列表
- Step 5: 运行分析验证并 Commit
git add -A && git commit -m "feat: 字体搜索/筛选(支持拼音匹配+分类过滤)"
Task 8: 滚动定位 + 下拉刷新
Files:
-
Modify:
lib/features/settings/presentation/font_management_page.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: FontManagementPage 改为 StatefulWidget + ScrollController
class FontManagementPage extends ConsumerStatefulWidget {
const FontManagementPage({super.key});
@override
ConsumerState<FontManagementPage> createState() => _FontManagementPageState();
}
class _FontManagementPageState extends ConsumerState<FontManagementPage> {
final _scrollController = ScrollController();
final _onlineSectionKey = GlobalKey();
void scrollToOnlineSection() {
if (_onlineSectionKey.currentContext != null) {
Scrollable.ensureVisible(
_onlineSectionKey.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
- Step 2: 下拉刷新(CustomRefreshIndicator)
CustomRefreshIndicator(
onRefresh: () => ref.read(fontManagementProvider.notifier).refresh(),
builder: (context, child, controller) {
return Stack(children: [
child,
if (controller.value > 0)
Positioned(top: 0, left: 0, right: 0,
child: CupertinoActivityIndicator().animate().fadeIn(),
),
]);
},
child: ListView(controller: _scrollController, children: [...]),
)
- Step 3: FontOnlineSection 传入 key
FontOnlineSection(key: _onlineSectionKey)
- Step 4: FontQuickActions 的在线字体按钮调用 scrollToOnlineSection
通过回调传递 scrollToOnlineSection 方法给 FontQuickActions。
- Step 5: 运行分析验证并 Commit
git add -A && git commit -m "feat: 滚动定位到在线字体区+下拉刷新字体列表"
Task 9: 动画增强(入场动画 + Hero动画 + 骨架屏)
Files:
-
Modify:
lib/features/settings/presentation/font_widgets.dart -
Modify:
lib/features/settings/presentation/font_management_page.dart -
Step 1: 字体列表项入场动画
使用项目封装的 SlideUpItem 或直接 flutter_animate:
FontItemWidget(font: font, ...)
.animate()
.fadeIn(duration: 300.ms, delay: (index * 50).ms)
.slideY(begin: 0.1, end: 0, curve: Curves.easeOutCubic)
- Step 2: 当前字体卡片 → 预览 Sheet 的 Hero 动画
Heroine(
tag: 'font-preview-${font.fontFamily}',
child: Text(font.name, style: ...),
)
在 FontPreviewSheet 中对应:
Heroine(
tag: 'font-preview-${widget.font.fontFamily}',
child: Text(widget.font.name, style: ...),
)
- Step 3: 字体列表加载骨架屏
使用项目封装的 SkeletonBox + ListItemSkeleton:
if (state.isLoading)
SkeletonBox(child: Column(children: [
for (var i = 0; i < 4; i++) const ListItemSkeleton(),
]))
else ...
- Step 4: 运行分析验证并 Commit
git add -A && git commit -m "feat: 字体列表入场动画+Hero过渡+骨架屏加载"
Task 10: 下载增强(并发控制 + 重试 + 进度圆环)
Files:
-
Modify:
lib/features/settings/presentation/font_models.dart -
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: FontManagementState 增加 downloadQueue
final Set<int> downloadQueue = const {};
- Step 2: Notifier 增加并发下载控制
static const _maxConcurrentDownloads = 3;
Future<void> downloadFont(int index) async {
if (state.downloadQueue.length >= _maxConcurrentDownloads) {
AppToast.showWarning('最多同时下载 $_maxConcurrentDownloads 个字体');
return;
}
final newQueue = Set<int>.from(state.downloadQueue)..add(index);
state = state.copyWith(downloadQueue: newQueue);
try {
// ... 原有下载逻辑
} finally {
final updatedQueue = Set<int>.from(state.downloadQueue)..remove(index);
state = state.copyWith(downloadQueue: updatedQueue);
}
}
- Step 3: 下载重试机制
Future<void> retryDownload(int index) async {
final updated = List<FontInfo>.from(state.onlineFonts);
updated[index] = state.onlineFonts[index].copyWith(
isDownloading: false, downloadProgress: 0.0,
);
state = state.copyWith(onlineFonts: updated);
await downloadFont(index);
}
在 UI 中,下载失败的字体项显示重试按钮。
- Step 4: 下载进度圆环(替代线性进度条)
SizedBox(
width: 28, height: 28,
child: Stack(alignment: Alignment.center, children: [
CircularProgressIndicator(
value: font.downloadProgress,
strokeWidth: 3,
backgroundColor: ext.textHint.withValues(alpha: 0.1),
valueColor: AlwaysStoppedAnimation<Color>(ext.accent),
),
Text('${(font.downloadProgress * 100).round()}',
style: AppTypography.caption2.copyWith(color: ext.accent, fontSize: 8)),
]),
)
- Step 5: 运行分析验证并 Commit
git add -A && git commit -m "feat: 并发下载控制(3个)+重试机制+进度圆环"
Phase 3: 数据动态化与高级功能
Task 11: 在线字体数据动态化(Supabase)
Files:
-
Create:
lib/features/settings/services/font_sync_service.dart -
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Modify:
lib/features/settings/presentation/font_models.dart -
Step 1: 创建 Supabase fonts 表结构
在 Supabase 控制台创建 fonts 表:
CREATE TABLE fonts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
font_family TEXT NOT NULL UNIQUE,
download_url TEXT NOT NULL,
icon_emoji TEXT DEFAULT '🔤',
author TEXT,
license TEXT,
category TEXT DEFAULT 'sans',
tags TEXT[] DEFAULT '{}',
language TEXT[] DEFAULT '{"zh","en"}',
file_size BIGINT,
version TEXT DEFAULT '1.0',
download_count INT DEFAULT 0,
rating REAL DEFAULT 0,
is_active BOOLEAN DEFAULT true,
sort_order INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
- Step 2: 创建 FontSyncService
class FontSyncService {
static SupabaseClient get _client => Supabase.instance.client;
static Future<List<OnlineFontEntry>> fetchOnlineFonts() async {
final response = await _client
.from('fonts')
.select()
.eq('is_active', true)
.order('sort_order', ascending: true);
return response.map<OnlineFontEntry>((e) => OnlineFontEntry.fromJson(e)).toList();
}
}
- Step 3: Notifier 中在线字体数据改为从 Supabase 加载
保留 onlineFontData 作为离线 fallback,优先从 Supabase 获取。
- Step 4: 运行分析验证并 Commit
git add -A && git commit -m "feat: 在线字体数据动态化(Supabase远程字体列表+离线fallback)"
Task 12: 字体收藏持久化
Files:
-
Modify:
lib/features/settings/presentation/font_models.dart -
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: FontInfo 增加 isFavorite 字段
已在 Task 2 的模型扩展中包含。
- Step 2: Notifier 增加收藏方法
void toggleFavorite(String fontFamily) {
final updatedFonts = state.fonts.map((f) {
if (f.fontFamily == fontFamily) {
return f.copyWith(isFavorite: !f.isFavorite);
}
return f;
}).toList();
state = state.copyWith(fonts: updatedFonts);
_saveInstalledFontsToKV(updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList());
_saveFavoritesToKV(updatedFonts);
}
void _saveFavoritesToKV(List<FontInfo> fonts) {
final favIds = fonts.where((f) => f.isFavorite).map((f) => f.fontFamily).toList();
AppKVStore.setStringList('font_favorites', favIds);
}
- Step 3: 收藏字体置顶显示
在 filteredFonts getter 中,收藏的字体排在前面。
- Step 4: 运行分析验证并 Commit
git add -A && git commit -m "feat: 字体收藏持久化(Hive)+收藏置顶显示"
Task 13: 字体包 ZIP 导入 + 批量删除 + 字体分享
Files:
-
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: ZIP 字体包导入
使用 archive 库:
Future<void> importFontZip() async {
if (pu.isWeb) return;
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom, allowedExtensions: ['zip'], allowMultiple: false,
);
if (result == null || result.files.isEmpty) return;
final filePath = result.files.first.path;
if (filePath == null) return;
final bytes = await File(filePath).readAsBytes();
final archive = ZipDecoder().decodeBytes(bytes);
final fontDir = await _getFontDirectory();
int count = 0;
for (final file in archive) {
if (file.isFile) {
final name = file.name.toLowerCase();
if (name.endsWith('.ttf') || name.endsWith('.otf')) {
final fileName = file.name.split('/').last;
final outputPath = '${fontDir.path}${Platform.pathSeparator}$fileName';
await File(outputPath).writeAsBytes(file.content as List<int>);
// 加载字体到引擎...
count++;
}
}
}
if (count > 0) AppToast.showSuccess('成功导入 $count 个字体 ✅');
} catch (e) {
Log.e('ZIP字体导入失败', e);
AppToast.showError('导入失败: $e');
}
}
- Step 2: 批量删除
Future<void> deleteFonts(List<String> fontFamilies) async {
for (final family in fontFamilies) {
final font = state.fonts.firstWhere((f) => f.fontFamily == family);
await deleteFont(family, font.path);
}
}
- Step 3: 字体分享
使用 share_plus:
Future<void> shareFont(FontInfo font) async {
if (font.sourceUrl != null && font.sourceUrl!.isNotEmpty) {
await SharePlus.instance.share(ShareParams(text: '推荐字体: ${font.name}\n下载地址: ${font.sourceUrl}'));
} else if (font.path.isNotEmpty) {
await SharePlus.instance.share(ShareParams(files: [XFile(font.path)]));
}
}
- Step 4: 运行分析验证并 Commit
git add -A && git commit -m "feat: ZIP字体包导入+批量删除+字体分享"
Task 14: Google Fonts 在线字体库集成
Files:
-
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: Notifier 增加 Google Fonts 加载方法
参考项目中 font_picker.dart 的 _safeGoogleFont 模式:
Future<void> loadGoogleFont(String fontName) async {
try {
final textStyle = GoogleFonts.getFont(fontName);
final fontFamily = textStyle.fontFamily;
if (fontFamily == null) return;
final loader = FontLoader(fontFamily);
// Google Fonts 会自动下载并加载
// 无需手动管理文件
final newFont = FontInfo(
name: fontName,
fontFamily: fontFamily,
path: 'google_fonts://$fontName',
isDownloaded: true,
category: 'google',
sourceUrl: 'https://fonts.google.com/specimen/$fontName',
);
final updatedFonts = List<FontInfo>.from(state.fonts)..add(newFont);
state = state.copyWith(fonts: updatedFonts);
AppToast.showSuccess('$fontName 加载成功 ✅');
} catch (e) {
Log.e('Google Font 加载失败: $fontName', e);
AppToast.showError('字体加载失败');
}
}
-
Step 2: 在线字体区增加 Google Fonts 分类入口
-
Step 3: 运行分析验证并 Commit
git add -A && git commit -m "feat: Google Fonts在线字体库集成(安全fallback)"
Task 15: 字体对比模式
Files:
-
Create:
lib/features/settings/presentation/font/font_comparison_page.dart -
Modify:
lib/features/settings/presentation/font_widgets.dart -
Step 1: 创建 FontComparisonPage
class FontComparisonPage extends ConsumerStatefulWidget {
const FontComparisonPage({super.key, required this.fontA, required this.fontB});
final FontInfo fontA;
final FontInfo fontB;
}
class _FontComparisonPageState extends ConsumerState<FontComparisonPage> {
final _previewController = TextEditingController(text: '闲言 AaBbCc 你好世界');
double _fontSize = 20.0;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: SafeArea(child: Row(children: [
Expanded(child: _buildFontPanel(widget.fontA)),
Container(width: 1, color: ext.textHint.withValues(alpha: 0.2)),
Expanded(child: _buildFontPanel(widget.fontB)),
])),
);
}
}
-
Step 2: 在字体项长按菜单中增加"对比"选项
-
Step 3: 运行分析验证并 Commit
git add -A && git commit -m "feat: 字体对比模式(左右分屏对比两种字体)"
Phase 4: 视觉规范与细节
Task 16: Emoji 替换为 Icon/SVG
Files:
- Create:
assets/svgs/font_import.svg,assets/svgs/font_url.svg,assets/svgs/font_online.svg,assets/svgs/font_preview.svg,assets/svgs/font_compare.svg,assets/svgs/font_favorite.svg,assets/svgs/font_zip.svg - Modify:
lib/shared/widgets/app_icon.dart - Modify:
lib/features/settings/presentation/font_widgets.dart - Modify:
lib/features/settings/presentation/font_models.dart
替换规则:
-
通用功能按钮/标签 → CupertinoIcons(系统自带)
-
个性化/品牌化图标 → 本地 SVG
-
保留 emoji 的场景:在线字体列表中的字体品牌标识(如 🖋️ 霞鹜文楷、💼 阿里巴巴普惠体)
-
Step 1: 创建 SVG 图标文件
按 iOS SF Symbols 风格设计,线条粗细 1.5px,24x24 viewBox:
-
font_import.svg— 文件夹+箭头导入 -
font_url.svg— 链接+下载 -
font_online.svg— 云+字体 -
font_preview.svg— 放大镜+文字 -
font_compare.svg— 双栏对比 -
font_favorite.svg— 星星+字体 -
font_zip.svg— ZIP包+字体 -
Step 2: AppIconData 增加字体相关常量
class AppIconData {
// ... 现有常量
static const String fontImport = 'assets/svgs/font_import.svg';
static const String fontUrl = 'assets/svgs/font_url.svg';
static const String fontOnline = 'assets/svgs/font_online.svg';
static const String fontPreview = 'assets/svgs/font_preview.svg';
static const String fontCompare = 'assets/svgs/font_compare.svg';
static const String fontFavorite = 'assets/svgs/font_favorite.svg';
static const String fontZip = 'assets/svgs/font_zip.svg';
}
- Step 3: font_widgets.dart 中替换 emoji
| 原始 emoji | 替换为 | 场景 |
|---|---|---|
| 📁 导入字体 | AppIcon(svgAsset: AppIconData.fontImport) |
快捷操作按钮 |
| 🔗 URL下载 | AppIcon(svgAsset: AppIconData.fontUrl) |
快捷操作按钮 |
| ☁️ 在线字体 | AppIcon(svgAsset: AppIconData.fontOnline) |
快捷操作按钮+区域标题 |
| ✨ 当前字体 | CupertinoIcons.sparkles |
区域标题 |
| 📱 已安装字体 | CupertinoIcons.device_phone_portrait |
区域标题 |
| 💡 字体小贴士 | AppIcon(svgAsset: AppIconData.lightbulb) |
区域标题 |
| 📂 支持.ttf | CupertinoIcons.doc_text |
小贴士项 |
| 🌐 在线下载 | CupertinoIcons.cloud_download |
小贴士项 |
| 🔄 切换生效 | CupertinoIcons.arrow_2_circlepath |
小贴士项 |
| 💾 存储目录 | CupertinoIcons.folder_fill |
小贴士项 |
| ⚠️ 科学上网 | CupertinoIcons.exclamationmark_triangle |
小贴士项 |
| 🖋️💼🎉🌸📐📜 | 保留 emoji | 在线字体品牌标识 |
| ⬇️ 下载中 | CupertinoIcons.arrow_down_circle |
下载进度 |
| 🗑️ 删除 | CupertinoIcons.trash |
删除确认 |
| 🔤 字体管理 | CupertinoIcons.textformat_abc |
导航栏标题 |
| 👇 向下滚动 | 移除(改为真正滚动) | Toast 提示 |
- Step 4: 运行分析验证并 Commit
git add -A && git commit -m "style: 字体页面emoji替换为CupertinoIcon/SVG,保留品牌emoji"
Task 17: 字体切换音效
Files:
-
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Step 1: 字体切换时播放轻柔反馈音
使用 audioplayers(项目已集成):
static final _audioPlayer = AudioPlayer();
Future<void> _playSwitchSound() async {
try {
await _audioPlayer.play(AssetSource('sounds/font_switch.mp3'));
} catch (_) {}
}
在 setActiveFont 成功后调用 _playSwitchSound()。
- Step 2: 运行分析验证并 Commit
git add -A && git commit -m "feat: 字体切换音效反馈"
Phase 5: 删除后字体残留处理
Task 18: 删除字体后的引擎处理
Files:
-
Modify:
lib/features/settings/presentation/font_management_notifier.dart -
Step 1: 记录已删除字体列表
Flutter FontLoader 不支持卸载字体,但可以通过记录已删除字体列表,在应用重启时不加载它们:
static const _kvKeyDeletedFonts = 'font_deleted_families';
void _recordDeletedFont(String fontFamily) {
final deleted = AppKVStore.getStringList(_kvKeyDeletedFonts) ?? [];
if (!deleted.contains(fontFamily)) {
deleted.add(fontFamily);
AppKVStore.setStringList(_kvKeyDeletedFonts, deleted);
}
}
void _removeFromDeletedList(String fontFamily) {
final deleted = AppKVStore.getStringList(_kvKeyDeletedFonts) ?? [];
deleted.remove(fontFamily);
AppKVStore.setStringList(_kvKeyDeletedFonts, deleted);
}
- Step 2: _init 中过滤已删除字体
在 _loadDynamicFonts 中,跳过已删除的字体:
Future<void> _loadDynamicFonts(List<FontInfo> fonts) async {
final deletedFamilies = AppKVStore.getStringList(_kvKeyDeletedFonts) ?? [];
for (final font in fonts) {
if (font.path.isNotEmpty && font.isDownloaded && !deletedFamilies.contains(font.fontFamily)) {
await _loadFontIntoEngine(font.fontFamily, font.path);
}
}
}
-
Step 3: 删除时记录 + 重新安装时移除记录
-
Step 4: 运行分析验证并 Commit
git add -A && git commit -m "fix: 删除字体后记录已删除列表,重启时不加载已删除字体"
自审清单
1. Spec 覆盖检查
| 需求 | 对应 Task |
|---|---|
| 1. _applyCustomFont 不完整 | Task 1 ✅ |
| 2. 序列化脆弱 | Task 2 ✅ |
| 3. 在线字体数据硬编码 | Task 11 ✅ |
| 4. 鸿蒙端不可用 | Task 4 ✅ |
| 5. 无字体预览大图 | Task 5 ✅ |
| 6. 无搜索/筛选 | Task 7 ✅ |
| 7. 无并发下载控制 | Task 10 ✅ |
| 8. 删除后字体残留 | Task 18 ✅ |
| 9. 内置字体映射硬编码 | Task 3 ✅ |
| 10. 滚动定位未实现 | Task 8 ✅ |
| 11. 无下载重试机制 | Task 10 ✅ |
| 12. 无批量操作 | Task 13 ✅ |
| 13. 无字体元数据展示 | Task 5 (预览Sheet中) + Task 11 (Supabase) ✅ |
| 14. 无自定义预览文本 | Task 5 ✅ |
| 字体卡片滑动删除 | Task 6 ✅ |
| 字体预览大图 Sheet | Task 5 ✅ |
| Google Fonts 集成 | Task 14 ✅ |
| 列表项入场动画 | Task 9 ✅ |
| 字体切换 Hero 动画 | Task 9 ✅ |
| 下拉刷新字体列表 | Task 8 ✅ |
| 在线字体数据动态化 | Task 11 ✅ |
| 字体收藏持久化 | Task 12 ✅ |
| 字体加载骨架屏 | Task 9 ✅ |
| 字体包 ZIP 导入 | Task 13 ✅ |
| 字体分享 | Task 13 ✅ |
| 字体对比模式 | Task 15 ✅ |
| 下载进度圆环 | Task 10 ✅ |
| 字体搜索拼音 | Task 7 ✅ |
| 字体切换音效 | Task 17 ✅ |
| emoji→icon替换 | Task 16 ✅ |
2. 占位符扫描
无 TBD/TODO/实现后补充等占位符。
3. 类型一致性
所有 Task 中引用的类名、方法名、字段名保持一致:
FontInfo.isFavorite/FontInfo.category/FontInfo.author等FontManagementState.searchQuery/filterCategory/downloadQueueThemeSettingsState.customFontFamilybuiltInFontConfigs替代硬编码idMap