908 lines
28 KiB
Dart
908 lines
28 KiB
Dart
/// ============================================================
|
||
/// 闲言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,
|
||
);
|