Files
xianyan/docs/spec/font_management_spec.md
Developer f9c19463f9 chore: 批量更新v6.5.21版本,整合多项功能修复与优化
主要变更:
1. 新增多风格音效资源与管理文档
2. 修复翻译服务空响应处理与Dio日志异常捕获
3. 完善Web端平台适配与路径获取Stub
4. 优化设备配对与文件传输功能
5. 新增角色命名常量与摇一摇检测器
6. 修复Riverpod dispose与鸿蒙导航路由
7. 新增每日通知服务与流体着色器
8. 优化备份服务与数据管理页面
9. 新增隐私设置附近设备发现选项
10. 重构诗词提供者支持历史记录
11. 完善桌面端构建配置与开发脚本
12. 清理旧版工具部署脚本
2026-05-21 00:19:14 +08:00

38 KiB
Raw Blame History

字体管理页面重构与功能增强 实施计划

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.dartThemeSettingsState 中:

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.dartFontInfo 中:

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.5px24x24 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 / downloadQueue
  • ThemeSettingsState.customFontFamily
  • builtInFontConfigs 替代硬编码 idMap