Files
xianyan/lib/features/settings/presentation/font_management_notifier.dart
2026-06-12 22:30:26 +08:00

908 lines
28 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 字体管理 Notifier
/// 创建时间: 2026-04-28
/// 更新时间: 2026-06-05
/// 作用: 字体导入/下载/删除/切换/动态加载等业务逻辑
/// 上次更新: Web兼容—Platform.pathSeparator→'/'
/// ============================================================
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../../core/storage/kv_storage.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/safe_init_mixin.dart';
import '../../../core/utils/platform/platform_utils.dart' as pu;
import '../../../shared/widgets/feedback/app_toast.dart';
import '../services/font_download_service.dart';
import '../services/font_sync_service.dart';
import '../providers/theme_settings_provider.dart';
import 'font_models.dart';
/// 字体管理 Notifier
class FontManagementNotifier extends Notifier<FontManagementState>
with SafeNotifierInit {
@override
FontManagementState build() {
safeNotifierInit(_init, label: 'FontManagementNotifier');
return const FontManagementState();
}
/// 设置搜索关键词
void setSearchQuery(String query) {
state = state.copyWith(searchQuery: query);
}
/// 下拉刷新:重新加载所有字体数据
Future<void> refresh() async {
state = state.copyWith(isLoading: true, searchQuery: '');
try {
final installedFonts = _loadInstalledFontsFromKV();
final localFonts = await _scanLocalFonts();
final favoriteIds = _loadFavoritesFromKV();
final allFonts = [..._builtInFonts, ...installedFonts, ...localFonts].map(
(f) {
if (favoriteIds.contains(f.fontFamily)) {
return f.copyWith(isFavorite: true);
}
return f;
},
).toList();
final onlineFonts = _buildOnlineFonts(installedFonts, localFonts);
state = state.copyWith(
fonts: allFonts,
onlineFonts: onlineFonts,
isLoading: false,
);
await _loadDynamicFonts(installedFonts);
await _loadDynamicFonts(localFonts);
} catch (e) {
Log.e('字体刷新失败', e);
state = state.copyWith(isLoading: false);
}
}
static const _kvKeyActiveFont = 'font_active_family';
static const _kvKeyInstalledFonts = 'font_installed_list';
static const _kvKeyDeletedFonts = 'font_deleted_families';
/// 字体切换音效播放器
static final _audioPlayer = AudioPlayer();
/// 播放字体切换音效
Future<void> _playSwitchSound() async {
try {
await _audioPlayer.play(AssetSource('sounds/font_switch.mp3'));
} catch (e) {
Log.d('字体切换音效播放失败(可忽略): $e');
}
}
/// 初始化字体数据
Future<void> _init() async {
state = state.copyWith(isLoading: true);
try {
final activeFont = KvStorage.getString(_kvKeyActiveFont) ?? 'Inter';
final installedFonts = _loadInstalledFontsFromKV();
final localFonts = await _scanLocalFonts();
final favoriteIds = _loadFavoritesFromKV();
final allFonts = [..._builtInFonts, ...installedFonts, ...localFonts].map(
(f) {
if (favoriteIds.contains(f.fontFamily)) {
return f.copyWith(isFavorite: true);
}
return f;
},
).toList();
final onlineFonts = _buildOnlineFonts(installedFonts, localFonts);
state = state.copyWith(
fonts: allFonts,
onlineFonts: onlineFonts,
isLoading: false,
activeFontFamily: activeFont,
);
await _loadDynamicFonts(installedFonts);
await _loadDynamicFonts(localFonts);
_loadOnlineFontsFromRemote();
} catch (e) {
Log.e('字体初始化失败', e);
state = state.copyWith(isLoading: false);
}
}
/// 内置字体列表(基于 builtInFontConfigs 动态生成)
List<FontInfo> get _builtInFonts => builtInFontConfigs
.map(
(c) => FontInfo(
name: c.name,
fontFamily: c.fontFamily,
path: '',
isBuiltIn: true,
),
)
.toList();
/// 从 KV 存储加载已安装字体列表JSON格式兼容旧|分隔符格式迁移)
List<FontInfo> _loadInstalledFontsFromKV() {
final raw = KvStorage.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('字体列表JSON解析失败尝试旧格式迁移', e);
return _migrateFromOldFormat();
}
}
/// 旧格式迁移:从 | 分隔符格式迁移到 JSON
List<FontInfo> _migrateFromOldFormat() {
final raw = KvStorage.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;
}
/// 扫描本地字体目录
Future<List<FontInfo>> _scanLocalFonts() async {
if (pu.isWeb) return [];
try {
final dir = await getApplicationDocumentsDirectory();
final fontDir = Directory('${dir.path}/fonts');
final List<FontInfo> fonts = [];
if (await fontDir.exists()) {
await for (final entity in fontDir.list()) {
if (entity is File) {
final ext = entity.path.toLowerCase();
if (ext.endsWith('.ttf') || ext.endsWith('.otf')) {
final fileName = entity.path.split('/').last;
final name = fileName.replaceAll(RegExp(r'\.(ttf|otf)$'), '');
final fontFamily = name.replaceAll(' ', '');
final size = await entity.length();
fonts.add(
FontInfo(
name: name,
fontFamily: fontFamily,
path: entity.path,
isDownloaded: true,
fileSize: size,
),
);
}
}
}
}
return fonts;
} catch (e) {
Log.e('扫描本地字体失败', e);
return [];
}
}
/// 构建在线字体列表(标记已安装状态)
List<FontInfo> _buildOnlineFonts(
List<FontInfo> installed,
List<FontInfo> local,
) {
final allCustomFamilies = {
...installed,
...local,
}.map((f) => f.fontFamily).toSet();
return onlineFontData.map((data) {
final fontFamily = data.$2;
final isInstalled = allCustomFamilies.contains(fontFamily);
return FontInfo(
name: data.$1,
fontFamily: fontFamily,
path: '',
downloadUrl: data.$3,
isDownloaded: isInstalled,
iconEmoji: data.$4,
);
}).toList();
}
/// 从自建API远程加载在线字体列表
Future<void> _loadOnlineFontsFromRemote() async {
try {
final List<OnlineFontEntry> entries =
await FontSyncService.fetchOnlineFonts();
if (entries.isEmpty) return;
final allCustomFamilies = state.fonts
.where((f) => !f.isBuiltIn && f.isDownloaded)
.map((f) => f.fontFamily)
.toSet();
final onlineFonts = entries
.map(
(OnlineFontEntry e) => e.toFontInfo(
isDownloaded: allCustomFamilies.contains(e.fontFamily),
),
)
.toList();
state = state.copyWith(onlineFonts: onlineFonts);
} catch (e) {
Log.e('远程字体列表加载失败', e);
}
}
/// 加载Google Font字体
Future<void> loadGoogleFont(String fontName) async {
try {
final textStyle = GoogleFonts.getFont(fontName);
final fontFamily = textStyle.fontFamily;
if (fontFamily == null) {
AppToast.showWarning('字体加载失败');
return;
}
final existing = state.fonts
.where((f) => f.fontFamily == fontFamily)
.firstOrNull;
if (existing != null) {
AppToast.showInfo('$fontName 已安装');
setActiveFont(fontFamily);
return;
}
final newFont = FontInfo(
name: fontName,
fontFamily: fontFamily,
path: 'google_fonts://$fontName',
isDownloaded: true,
category: 'google',
);
final updatedFonts = List<FontInfo>.from(state.fonts)..add(newFont);
state = state.copyWith(fonts: updatedFonts);
_saveInstalledFontsToKV(
updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList(),
);
AppToast.showSuccess('$fontName 加载成功');
} catch (e) {
Log.e('Google Font 加载失败: $fontName', e);
AppToast.showError('字体加载失败,请检查网络');
}
}
/// 动态加载字体到引擎
Future<void> _loadDynamicFonts(List<FontInfo> fonts) async {
final deletedFamilies = KvStorage.getStringList(_kvKeyDeletedFonts) ?? [];
for (final font in fonts) {
if (font.path.isNotEmpty &&
font.isDownloaded &&
!deletedFamilies.contains(font.fontFamily)) {
await FontDownloadService.loadFontIntoEngine(
font.fontFamily,
font.path,
);
}
}
}
/// 统一获取字体目录(自动创建)
Future<Directory> _getFontDirectory() async {
return FontDownloadService.getFontDirectory();
}
/// 下载在线字体委托给FontDownloadService
Future<void> downloadFont(int index) async {
if (index < 0 || index >= state.onlineFonts.length) return;
if (pu.isWeb) {
AppToast.showInfo('Web端暂不支持字体下载');
return;
}
final onlineFont = state.onlineFonts[index];
if (onlineFont.isDownloaded) {
AppToast.showSuccess('${onlineFont.name} 已安装');
return;
}
if (onlineFont.isDownloading) return;
if (state.downloadQueue.length >= 3) {
AppToast.showWarning('最多同时下载 3 个字体,请稍候');
return;
}
final newQueue = Set<int>.from(state.downloadQueue)..add(index);
final newOnlineFonts = List<FontInfo>.from(state.onlineFonts);
newOnlineFonts[index] = onlineFont.copyWith(
isDownloading: true,
downloadProgress: 0.0,
hasDownloadError: false,
);
state = state.copyWith(
onlineFonts: newOnlineFonts,
downloadQueue: newQueue,
);
try {
final downloadUrl = state.onlineFonts[index].downloadUrl;
final primaryUrl = downloadUrl.isNotEmpty
? downloadUrl
: (index < onlineFontData.length ? onlineFontData[index].$3 : '');
final fallbacks = fontFallbackUrls[onlineFont.fontFamily] ?? [];
final result = await FontDownloadService.downloadFont(
fontFamily: onlineFont.fontFamily,
displayName: onlineFont.name,
primaryUrl: primaryUrl,
fallbackUrls: fallbacks,
onProgress: (progress) {
final updated = List<FontInfo>.from(state.onlineFonts);
updated[index] = state.onlineFonts[index].copyWith(
downloadProgress: progress,
);
state = state.copyWith(onlineFonts: updated);
},
);
if (!result.success) {
_resetDownloadState(index, result.errorMsg ?? '下载失败');
return;
}
final loaded = await FontDownloadService.loadFontIntoEngine(
onlineFont.fontFamily,
result.savePath!,
);
if (loaded) {
final newFont = FontInfo(
name: onlineFont.name,
fontFamily: onlineFont.fontFamily,
path: result.savePath!,
isDownloaded: true,
fileSize: result.fileSize,
);
final updatedOnlineFonts = List<FontInfo>.from(state.onlineFonts);
updatedOnlineFonts[index] = newFont.copyWith(
isDownloading: false,
downloadProgress: 1.0,
);
final updatedFonts = List<FontInfo>.from(state.fonts)..add(newFont);
state = state.copyWith(
fonts: updatedFonts,
onlineFonts: updatedOnlineFonts,
downloadQueue: Set<int>.from(state.downloadQueue)..remove(index),
);
_saveInstalledFontsToKV(
updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList(),
);
_removeFromDeletedList(onlineFont.fontFamily);
AppToast.showSuccess('${onlineFont.name} 下载安装成功 ✅');
} else {
_resetDownloadState(index, '${onlineFont.name} 加载失败');
}
} catch (e) {
Log.e('字体安装异常: ${onlineFont.name}', e);
_resetDownloadState(index, '安装失败: $e');
}
}
/// 重置下载状态
void _resetDownloadState(int index, String errorMsg) {
if (index < 0 || index >= state.onlineFonts.length) return;
final updated = List<FontInfo>.from(state.onlineFonts);
updated[index] = state.onlineFonts[index].copyWith(
isDownloading: false,
hasDownloadError: true,
);
state = state.copyWith(
onlineFonts: updated,
downloadQueue: Set<int>.from(state.downloadQueue)..remove(index),
);
AppToast.showError(errorMsg);
}
/// 重试下载字体
void retryDownload(int index) {
if (index < 0 || index >= state.onlineFonts.length) return;
final updated = List<FontInfo>.from(state.onlineFonts);
updated[index] = state.onlineFonts[index].copyWith(
isDownloading: false,
hasDownloadError: false,
);
state = state.copyWith(onlineFonts: updated);
downloadFont(index);
}
/// 从文件选择器导入字体委托给FontDownloadService
Future<void> importFont() async {
if (pu.isWeb) {
AppToast.showInfo('Web端暂不支持字体导入');
return;
}
try {
final results = await FontDownloadService.importLocalFonts();
int successCount = 0;
for (final result in results) {
if (!result.success) {
if (result.errorMsg != null) {
AppToast.showError(result.errorMsg!);
}
continue;
}
final loaded = await FontDownloadService.loadFontIntoEngine(
result.fontFamily!,
result.savePath!,
);
if (loaded) {
final newFont = FontInfo(
name: result.displayName!,
fontFamily: result.fontFamily!,
path: result.savePath!,
isDownloaded: true,
fileSize: result.fileSize,
);
final existingIndex = state.fonts.indexWhere(
(f) => f.fontFamily == result.fontFamily,
);
final updatedFonts = List<FontInfo>.from(state.fonts);
if (existingIndex >= 0) {
updatedFonts[existingIndex] = newFont;
} else {
updatedFonts.add(newFont);
}
state = state.copyWith(fonts: updatedFonts);
_saveInstalledFontsToKV(
updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList(),
);
_removeFromDeletedList(result.fontFamily!);
successCount++;
}
}
if (successCount > 0) {
AppToast.showSuccess('成功导入 $successCount 个字体 ✅');
} else if (results.isNotEmpty) {
AppToast.showError('字体导入失败');
}
} catch (e) {
Log.e('字体导入失败', e);
AppToast.showError('导入失败: $e');
}
}
/// 从URL下载字体委托给FontDownloadService
Future<void> downloadFontFromUrl(String url, {String? name}) async {
if (pu.isWeb) {
AppToast.showInfo('Web端暂不支持字体下载');
return;
}
try {
final displayName = name?.isNotEmpty == true ? name! : url;
state = state.copyWith(
isUrlDownloading: true,
urlDownloadProgress: 0.0,
urlDownloadName: displayName,
);
final result = await FontDownloadService.downloadFontFromUrl(
url: url,
name: name,
onProgress: (progress) {
state = state.copyWith(urlDownloadProgress: progress);
},
);
if (!result.success) {
state = state.copyWith(
isUrlDownloading: false,
urlDownloadProgress: 0.0,
);
AppToast.showError(result.errorMsg ?? '下载失败');
return;
}
final loaded = await FontDownloadService.loadFontIntoEngine(
result.fontFamily!,
result.savePath!,
);
if (loaded) {
final newFont = FontInfo(
name: result.displayName!,
fontFamily: result.fontFamily!,
path: result.savePath!,
isDownloaded: true,
fileSize: result.fileSize,
);
final updatedFonts = List<FontInfo>.from(state.fonts);
final existingIndex = updatedFonts.indexWhere(
(f) => f.fontFamily == result.fontFamily,
);
if (existingIndex >= 0) {
updatedFonts[existingIndex] = newFont;
} else {
updatedFonts.add(newFont);
}
state = state.copyWith(
fonts: updatedFonts,
isUrlDownloading: false,
urlDownloadProgress: 1.0,
);
_saveInstalledFontsToKV(
updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList(),
);
_removeFromDeletedList(result.fontFamily!);
AppToast.showSuccess('${result.displayName} 下载安装成功 ✅');
} else {
final file = File(result.savePath!);
if (await file.exists()) await file.delete();
state = state.copyWith(
isUrlDownloading: false,
urlDownloadProgress: 0.0,
);
AppToast.showError('字体加载失败,文件可能不是有效的字体格式');
}
} catch (e) {
Log.e('URL字体下载异常', e);
state = state.copyWith(isUrlDownloading: false, urlDownloadProgress: 0.0);
AppToast.showError('下载失败: $e');
}
}
/// 删除字体
Future<void> deleteFont(String fontFamily, String path) async {
if (pu.isWeb) return;
try {
final file = File(path);
if (await file.exists()) {
await file.delete();
}
final updatedFonts = state.fonts
.where((f) => f.fontFamily != fontFamily)
.toList();
final updatedOnlineFonts = state.onlineFonts.map((f) {
if (f.fontFamily == fontFamily) {
return f.copyWith(
isDownloaded: false,
isDownloading: false,
downloadProgress: 0.0,
);
}
return f;
}).toList();
if (state.activeFontFamily == fontFamily) {
setActiveFont('Inter');
}
state = state.copyWith(
fonts: updatedFonts,
onlineFonts: updatedOnlineFonts,
);
_saveInstalledFontsToKV(
updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList(),
);
_recordDeletedFont(fontFamily);
AppToast.showSuccess('字体已删除 🗑️');
} catch (e) {
Log.e('字体删除失败', e);
AppToast.showError('删除失败');
}
}
/// 批量删除字体
Future<void> deleteFonts(List<String> fontFamilies) async {
for (final family in fontFamilies) {
final font = state.fonts.firstWhere(
(f) => f.fontFamily == family,
orElse: () => const FontInfo(name: '', fontFamily: '', path: ''),
);
if (font.path.isNotEmpty) {
await deleteFont(family, font.path);
}
}
}
/// ZIP字体包导入
Future<void> importFontZip() async {
if (pu.isWeb) {
AppToast.showInfo('Web端不支持字体导入');
return;
}
if (pu.isOhos) {
AppToast.showInfo('鸿蒙端暂不支持ZIP字体导入');
return;
}
try {
final result = await FilePicker.pickFiles(dialogTitle: '选择ZIP字体包');
if (result == null || result.files.isEmpty) return;
final platformFile = result.files.first;
// 手动校验 zip 扩展名
if (!platformFile.name.toLowerCase().endsWith('.zip')) {
AppToast.showWarning('请选择 .zip 格式的字体包');
return;
}
Uint8List? zipBytes;
final filePath = platformFile.path;
if (filePath != null && filePath.isNotEmpty) {
final zipFile = File(filePath);
if (await zipFile.exists()) {
zipBytes = await zipFile.readAsBytes();
}
}
// 降级使用readAsBytes读取
// final bytes = await file.readAsBytes();
// 路径不可用时,使用 bytes 属性读取file_picker 11.x API
if (zipBytes == null) {
final rawBytes = platformFile.bytes;
if (rawBytes != null && rawBytes.isNotEmpty) {
zipBytes = rawBytes;
}
}
if (zipBytes == null || zipBytes.isEmpty) {
Log.w('ZIP字体导入跳过: 无可用文件路径或数据');
AppToast.showWarning('无法读取ZIP文件数据');
return;
}
final archive = ZipDecoder().decodeBytes(zipBytes);
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')) {
try {
final fileName = file.name.split('/').last;
final outputPath = '${fontDir.path}/$fileName';
await File(outputPath).writeAsBytes(file.content as List<int>);
final fontFamily = fileName.replaceAll(
RegExp(r'\.(ttf|otf)$'),
'',
);
final loaded = await FontDownloadService.loadFontIntoEngine(
fontFamily,
outputPath,
);
if (!loaded) continue;
_removeFromDeletedList(fontFamily);
final newFont = FontInfo(
name: fontFamily,
fontFamily: fontFamily,
path: outputPath,
isDownloaded: true,
fileSize: await File(outputPath).length(),
);
final existingIndex = state.fonts.indexWhere(
(f) => f.fontFamily == fontFamily,
);
final updatedFonts = List<FontInfo>.from(state.fonts);
if (existingIndex >= 0) {
updatedFonts[existingIndex] = newFont;
} else {
updatedFonts.add(newFont);
}
state = state.copyWith(fonts: updatedFonts);
count++;
} catch (e) {
Log.e('ZIP内单个字体导入失败: ${file.name}', e);
}
}
}
}
if (count > 0) {
final allInstalled = state.fonts
.where((f) => !f.isBuiltIn && f.isDownloaded)
.toList();
_saveInstalledFontsToKV(allInstalled);
AppToast.showSuccess('成功导入 $count 个字体 📦');
} else {
AppToast.showWarning('ZIP包中未找到 .ttf 或 .otf 字体文件');
}
} catch (e) {
Log.e('ZIP字体导入失败', e);
AppToast.showError('导入失败: $e');
}
}
/// 字体分享
Future<void> shareFont(FontInfo font) async {
try {
if (font.path.isNotEmpty && !pu.isWeb) {
await SharePlus.instance.share(ShareParams(files: [XFile(font.path)]));
} else {
await SharePlus.instance.share(ShareParams(text: '推荐字体: ${font.name}'));
}
} catch (e) {
Log.e('字体分享失败', e);
AppToast.showError('分享失败');
}
}
/// 设置当前活跃字体
void setActiveFont(String fontFamily) async {
state = state.copyWith(activeFontFamily: fontFamily);
KvStorage.setString(_kvKeyActiveFont, fontFamily);
final isBuiltIn = _builtInFonts.any((f) => f.fontFamily == fontFamily);
if (isBuiltIn) {
ref.read(themeSettingsProvider.notifier).setCustomFontFamily('');
final fontStyleId = _builtInFonts
.firstWhere(
(f) => f.fontFamily == fontFamily,
orElse: () => const FontInfo(
name: '',
fontFamily: 'Inter',
path: '',
isBuiltIn: true,
),
)
.fontFamily;
final config = builtInFontConfigs
.where((c) => c.fontFamily == fontStyleId)
.firstOrNull;
final id = config?.styleId ?? 'system';
ref.read(themeSettingsProvider.notifier).setFontStyle(id);
} else {
final fontInfo = state.fonts.firstWhere(
(f) => f.fontFamily == fontFamily,
orElse: () => const FontInfo(name: '', fontFamily: '', path: ''),
);
if (fontInfo.path.isEmpty && !fontInfo.isBuiltIn) {
final isGoogleFont =
fontInfo.path.startsWith('google_fonts://') ||
state.fonts.any(
(f) =>
f.fontFamily == fontFamily &&
f.path.startsWith('google_fonts://'),
);
if (!isGoogleFont) {
AppToast.showError('字体文件不存在,切换失败');
return;
}
}
if (fontInfo.path.isNotEmpty) {
await FontDownloadService.loadFontIntoEngine(fontFamily, fontInfo.path);
}
_applyCustomFont(fontFamily);
}
final fontName = state.fonts
.firstWhere(
(f) => f.fontFamily == fontFamily,
orElse: () => const FontInfo(name: '未知', fontFamily: '', path: ''),
)
.name;
AppToast.showSuccess('已切换为 $fontName');
_playSwitchSound();
}
/// 应用自定义字体
void _applyCustomFont(String fontFamily) {
try {
ref.read(themeSettingsProvider.notifier).setCustomFontFamily(fontFamily);
Log.i('自定义字体已应用: $fontFamily (通过 customFontFamily 传递给主题系统)');
} catch (e) {
Log.e('自定义字体应用失败', e);
}
}
/// 保存已安装字体列表到 KV 存储JSON格式
void _saveInstalledFontsToKV(List<FontInfo> fonts) {
final json = jsonEncode(fonts.map((f) => f.toJson()).toList());
KvStorage.setString(_kvKeyInstalledFonts, json);
}
/// 切换字体收藏状态
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);
}
/// 保存收藏列表到 KV 存储
void _saveFavoritesToKV(List<FontInfo> fonts) {
final favIds = fonts
.where((f) => f.isFavorite)
.map((f) => f.fontFamily)
.toList();
KvStorage.setStringList('font_favorites', favIds);
}
/// 从 KV 存储加载收藏列表
List<String> _loadFavoritesFromKV() {
return KvStorage.getStringList('font_favorites') ?? [];
}
/// 记录已删除字体到 KV 存储
void _recordDeletedFont(String fontFamily) {
final deleted = KvStorage.getStringList(_kvKeyDeletedFonts) ?? [];
if (!deleted.contains(fontFamily)) {
deleted.add(fontFamily);
KvStorage.setStringList(_kvKeyDeletedFonts, deleted);
}
}
/// 从已删除列表中移除字体
void _removeFromDeletedList(String fontFamily) {
final deleted = KvStorage.getStringList(_kvKeyDeletedFonts) ?? [];
deleted.remove(fontFamily);
KvStorage.setStringList(_kvKeyDeletedFonts, deleted);
}
}
/// 字体管理 Provider
final fontManagementProvider =
NotifierProvider<FontManagementNotifier, FontManagementState>(
FontManagementNotifier.new,
);