diff --git a/assets/fonts/LXGWWenKai-Regular.ttf b/assets/fonts/LXGWWenKai-Regular.ttf deleted file mode 100644 index eb61629..0000000 Binary files a/assets/fonts/LXGWWenKai-Regular.ttf and /dev/null differ diff --git a/assets/fonts/NotoSerifSC-Regular.ttf b/assets/fonts/NotoSerifSC-Regular.ttf deleted file mode 100644 index d667cb5..0000000 Binary files a/assets/fonts/NotoSerifSC-Regular.ttf and /dev/null differ diff --git a/assets/fonts/OPPO_Sans4.0.ttf b/assets/fonts/OPPO_Sans4.0.ttf deleted file mode 100644 index e649ce5..0000000 Binary files a/assets/fonts/OPPO_Sans4.0.ttf and /dev/null differ diff --git a/assets/fonts/SmileySans-Oblique.ttf b/assets/fonts/SmileySans-Oblique.ttf deleted file mode 100644 index c297dc6..0000000 Binary files a/assets/fonts/SmileySans-Oblique.ttf and /dev/null differ diff --git a/lib/pages/font_picker_page.dart b/lib/pages/font_picker_page.dart new file mode 100644 index 0000000..a8e3a8a --- /dev/null +++ b/lib/pages/font_picker_page.dart @@ -0,0 +1,399 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:path/path.dart' as path; +import 'package:permission_handler/permission_handler.dart'; +import '../utils/font_download_manager.dart'; +import '../utils/toast_util.dart'; + +/// 字体选择页面 +/// +/// 用户输入或选择字体目录,遍历目录下的字体文件,点击即可加载使用。 +class FontPickerPage extends StatefulWidget { + final String? initialFamily; + const FontPickerPage({super.key, this.initialFamily}); + + @override + State createState() => _FontPickerPageState(); +} + +class _FontPickerPageState extends State { + final TextEditingController _pathController = TextEditingController(); + final FontDownloadManager _fontManager = FontDownloadManager(); + + List _fonts = []; + String? _loadingPath; + String? _selectedFamily; + bool _isScanning = false; + + @override + void initState() { + super.initState(); + _selectedFamily = widget.initialFamily; + // 默认填入内置字体目录 + _pathController.text = '/sdcard/Documents/mooknote/fonts'; + _checkPermissionAndScan(); + } + + @override + void dispose() { + _pathController.dispose(); + super.dispose(); + } + + /// 请求存储权限(Android 11+ 需要 MANAGE_EXTERNAL_STORAGE) + Future _requestStoragePermission() async { + if (!Platform.isAndroid) return true; + + var status = await Permission.manageExternalStorage.status; + if (status.isGranted) return true; + + status = await Permission.manageExternalStorage.request(); + if (status.isGranted) return true; + + status = await Permission.storage.status; + if (status.isGranted) return true; + + status = await Permission.storage.request(); + return status.isGranted; + } + + /// 检查权限并扫描 + Future _checkPermissionAndScan() async { + final hasPermission = await _requestStoragePermission(); + if (!hasPermission) { + if (mounted) { + _showPermissionDialog(); + } + return; + } + await _scanDirectory(); + } + + /// 显示权限提示弹窗 + void _showPermissionDialog() { + final colors = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: colors.surface, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('需要存储权限', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: Text( + 'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能扫描字体文件。\n\n是否前往设置?', + style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4))), + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + openAppSettings(); + }, + child: Text('前往设置', style: TextStyle(color: colors.primary)), + ), + ], + ), + ); + } + + /// 扫描目录字体 + Future _scanDirectory() async { + final dirPath = _pathController.text.trim(); + if (dirPath.isEmpty) { + if (mounted) ToastUtil.show(context, '请输入目录路径'); + return; + } + + setState(() => _isScanning = true); + try { + final fonts = await _fontManager.scanFontDirectory(dirPath); + if (mounted) { + setState(() { + _fonts = fonts; + _isScanning = false; + }); + if (fonts.isEmpty) { + ToastUtil.show(context, '未找到字体文件'); + } + } + } catch (e) { + if (mounted) { + setState(() => _isScanning = false); + ToastUtil.show(context, '扫描失败: $e'); + } + } + } + + /// 使用 file_picker 选择目录 + Future _pickDirectory() async { + final hasPermission = await _requestStoragePermission(); + if (!hasPermission) { + if (mounted) _showPermissionDialog(); + return; + } + try { + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null && result.isNotEmpty) { + _pathController.text = result; + await _scanDirectory(); + } + } catch (e) { + if (mounted) ToastUtil.show(context, '选择目录失败: $e'); + } + } + + /// 加载并应用字体 + Future _loadFont(FontFileInfo font) async { + if (_loadingPath != null) return; // 防止重复点击 + + setState(() => _loadingPath = font.path); + try { + final family = await _fontManager.loadFontFile(font.path); + if (family != null) { + setState(() => _selectedFamily = family); + if (mounted) { + ToastUtil.show(context, '已应用: ${font.displayName}'); + Navigator.pop(context, family); + } + } else { + if (mounted) ToastUtil.show(context, '字体加载失败'); + } + } catch (e) { + if (mounted) ToastUtil.show(context, '加载失败: $e'); + } finally { + setState(() => _loadingPath = null); + } + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + return Scaffold( + backgroundColor: colors.surface, + appBar: AppBar( + title: const Text('选择字体'), + actions: [ + // 默认字体按钮 + TextButton( + onPressed: () => Navigator.pop(context, ''), + child: Text( + '恢复默认', + style: TextStyle( + fontSize: 13, + color: colors.primary, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + body: Column( + children: [ + // 路径输入区 + Container( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), + decoration: BoxDecoration( + color: colors.surface, + border: Border( + bottom: BorderSide(color: colors.outlineVariant, width: 0.5), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '字体目录', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: colors.onSurface.withValues(alpha: 0.5), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: _pathController, + style: TextStyle(fontSize: 13, color: colors.onSurface), + decoration: InputDecoration( + hintText: '输入字体目录路径', + hintStyle: TextStyle( + fontSize: 13, + color: colors.onSurface.withValues(alpha: 0.3), + ), + filled: true, + fillColor: colors.surfaceContainerHighest, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + suffixIcon: IconButton( + icon: Icon( + Icons.folder_open_outlined, + size: 18, + color: colors.onSurface.withValues(alpha: 0.5), + ), + onPressed: _pickDirectory, + ), + ), + onSubmitted: (_) => _scanDirectory(), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _scanDirectory, + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, + foregroundColor: colors.onPrimary, + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('扫描', style: TextStyle(fontSize: 13)), + ), + ], + ), + ], + ), + ), + + // 字体列表 + Expanded( + child: _isScanning + ? Center( + child: CircularProgressIndicator( + strokeWidth: 2, + color: colors.primary, + ), + ) + : _fonts.isEmpty + ? _buildEmptyState(colors) + : ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: _fonts.length, + separatorBuilder: (_, __) => Divider( + height: 0.5, + indent: 20, + endIndent: 20, + color: colors.outlineVariant, + ), + itemBuilder: (_, index) => _buildFontItem( + _fonts[index], + colors, + ), + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState(ColorScheme colors) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.folder_open_outlined, + size: 48, + color: colors.onSurface.withValues(alpha: 0.15), + ), + const SizedBox(height: 16), + Text( + '未找到字体文件', + style: TextStyle( + fontSize: 14, + color: colors.onSurface.withValues(alpha: 0.4), + ), + ), + const SizedBox(height: 4), + Text( + '支持 .ttf / .otf / .ttc 格式', + style: TextStyle( + fontSize: 12, + color: colors.onSurface.withValues(alpha: 0.25), + ), + ), + ], + ), + ); + } + + Widget _buildFontItem(FontFileInfo font, ColorScheme colors) { + final isLoading = _loadingPath == font.path; + final isSelected = _selectedFamily != null && + path.basenameWithoutExtension(font.fileName) == _selectedFamily; + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isSelected + ? colors.primary.withValues(alpha: 0.1) + : colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10), + border: isSelected + ? Border.all(color: colors.primary.withValues(alpha: 0.3), width: 1) + : null, + ), + child: isLoading + ? Center( + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colors.primary, + ), + ), + ) + : Icon( + Icons.font_download_outlined, + size: 18, + color: isSelected + ? colors.primary + : colors.onSurface.withValues(alpha: 0.5), + ), + ), + title: Text( + font.displayName, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + color: colors.onSurface, + ), + ), + subtitle: Text( + font.fileName, + style: TextStyle( + fontSize: 11, + color: colors.onSurface.withValues(alpha: 0.35), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: isSelected + ? Icon(Icons.check_circle, size: 18, color: colors.primary) + : null, + onTap: isLoading ? null : () => _loadFont(font), + ); + } +} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index 85f39c5..29ebcb1 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -7,6 +7,7 @@ import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:permission_handler/permission_handler.dart'; import '../main.dart' show routeObserver; import '../models/data_models.dart'; import '../providers/app_provider.dart'; @@ -25,6 +26,7 @@ import 'sync/cloud_sync_page.dart'; import 'app_icon_picker_page.dart'; import 'tag_management_page.dart'; import 'stroll_page.dart'; +import 'font_picker_page.dart'; /// 个人中心页面 class ProfilePage extends StatefulWidget { @@ -680,11 +682,7 @@ class _ProfilePageState extends State with RouteAware { () => Navigator.push( context, MaterialPageRoute(builder: (_) => const SettingsPage())) ), - ( - Icons.feedback_outlined, - '反馈', - () => _showFeedbackDialog(context) - ), + (Icons.feedback_outlined, '反馈', () => _showFeedbackDialog(context)), ]; return Padding( @@ -808,7 +806,8 @@ class _ProfilePageState extends State with RouteAware { Text('作者邮箱', style: TextStyle( fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.5))), + color: + colors.onSurface.withValues(alpha: 0.5))), const SizedBox(height: 2), Text(email, style: TextStyle( @@ -824,7 +823,8 @@ class _ProfilePageState extends State with RouteAware { ToastUtil.show(context, '已复制到剪贴板'); }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), decoration: BoxDecoration( color: colors.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), @@ -834,7 +834,11 @@ class _ProfilePageState extends State with RouteAware { children: [ Icon(Icons.copy, size: 14, color: colors.primary), const SizedBox(width: 4), - Text('复制', style: TextStyle(fontSize: 12, color: colors.primary, fontWeight: FontWeight.w600)), + Text('复制', + style: TextStyle( + fontSize: 12, + color: colors.primary, + fontWeight: FontWeight.w600)), ], ), ), @@ -864,7 +868,8 @@ class _ProfilePageState extends State with RouteAware { Text('QQ 群', style: TextStyle( fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.5))), + color: + colors.onSurface.withValues(alpha: 0.5))), const SizedBox(height: 2), Text('1087203310', style: TextStyle( @@ -880,7 +885,8 @@ class _ProfilePageState extends State with RouteAware { ToastUtil.show(context, '已复制到剪贴板'); }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), decoration: BoxDecoration( color: colors.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), @@ -890,7 +896,11 @@ class _ProfilePageState extends State with RouteAware { children: [ Icon(Icons.copy, size: 14, color: colors.primary), const SizedBox(width: 4), - Text('复制', style: TextStyle(fontSize: 12, color: colors.primary, fontWeight: FontWeight.w600)), + Text('复制', + style: TextStyle( + fontSize: 12, + color: colors.primary, + fontWeight: FontWeight.w600)), ], ), ), @@ -1225,6 +1235,17 @@ class _SettingsPageState extends State { subtitle: '清理未在数据库中引用的文件', onTap: () => _showClearCacheDialog(context), ), + Divider( + height: 0.5, + indent: 24, + endIndent: 24, + color: colors.outlineVariant), + _buildActionItem( + icon: Icons.folder_outlined, + title: '获取系统权限', + subtitle: '前往系统设置开启存储权限', + onTap: _showStoragePermissionDialog, + ), Divider( height: 0.5, indent: 24, @@ -1758,28 +1779,20 @@ class _SettingsPageState extends State { // ─── 字体选择器 ─── - static const _fontLabels = ['默认字体', '霞鹜文楷', 'OPPO Sans', '思源宋体', '得意黑']; - static const _fontValues = [ - '', - 'LXGWWenKai', - 'OPPOSans', - 'NotoSerifSC', - 'SmileySans' - ]; - static const _fontIcons = [ - Icons.font_download_outlined, - Icons.brush_outlined, - Icons.phone_android, - Icons.text_fields, - Icons.emoji_emotions_outlined - ]; - Widget _buildFontSelector() { final colors = Theme.of(context).colorScheme; - final idx = _fontValues.indexOf(_fontFamily); - final label = idx >= 0 ? _fontLabels[idx] : '系统默认'; + final label = _fontFamily.isEmpty ? '系统默认' : _fontFamily; return InkWell( - onTap: _showFontPicker, + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const FontPickerPage(initialFamily: '')), + ); + if (result != null && mounted) { + _setFontFamily(result); + } + }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row(children: [ @@ -1814,76 +1827,6 @@ class _SettingsPageState extends State { ); } - void _showFontPicker() { - final colors = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (ctx) => Container( - decoration: BoxDecoration( - color: colors.surface, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16))), - padding: const EdgeInsets.only(bottom: 20), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 36, - height: 4, - margin: const EdgeInsets.only(top: 12, bottom: 16), - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(2))), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.centerLeft, - child: Text('字体', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colors.onSurface)))), - const SizedBox(height: 8), - for (int i = 0; i < _fontLabels.length; i++) ...[ - if (i > 0) - Divider( - height: 0.5, - indent: 24, - endIndent: 24, - color: colors.outlineVariant), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(10)), - child: Icon(_fontIcons[i], - size: 20, - color: _fontFamily == _fontValues[i] - ? colors.primary - : colors.onSurface.withValues(alpha: 0.6))), - title: Text(_fontLabels[i], - style: TextStyle( - fontSize: 14, - fontWeight: _fontFamily == _fontValues[i] - ? FontWeight.w600 - : FontWeight.w400, - color: colors.onSurface)), - trailing: _fontFamily == _fontValues[i] - ? Icon(Icons.check, size: 20, color: colors.primary) - : null, - onTap: () { - Navigator.pop(ctx); - _setFontFamily(_fontValues[i]); - }, - ), - ], - ]), - ), - ); - } - void _setFontFamily(String family) { setState(() => _fontFamily = family); context.read().setFontFamily(family); @@ -2040,6 +1983,46 @@ class _SettingsPageState extends State { ); } + void _showStoragePermissionDialog() { + final colors = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: colors.surface, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('需要存储权限', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colors.onSurface)), + content: Text( + 'Android 11+ 需要在系统设置中授予"所有文件访问权限"才能扫描字体文件。\n\n是否前往设置?', + style: TextStyle( + fontSize: 14, + color: colors.onSurface.withValues(alpha: 0.6), + height: 1.6), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('取消', + style: + TextStyle(color: colors.onSurface.withValues(alpha: 0.4))), + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + // 跳转到应用设置页(用户可在权限中找到"所有文件访问") + openAppSettings(); + }, + child: Text('前往设置', style: TextStyle(color: colors.primary)), + ), + ], + ), + ); + } + void _showClearCacheDialog(BuildContext pageContext) { final colors = Theme.of(context).colorScheme; showDialog( @@ -2174,8 +2157,8 @@ class _SettingsPageState extends State { try { final db = await DatabaseHelper.instance.database; // 收集数据库中所有引用的 epub_books 子目录名(包括软删除的) - final rows = await db - .query('reader_books', columns: ['id', 'file_path', 'cover_path', 'is_deleted']); + final rows = await db.query('reader_books', + columns: ['id', 'file_path', 'cover_path', 'is_deleted']); final usedDirs = {}; for (final r in rows) { // 只收集未删除的记录对应的目录 diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 16de901..0272629 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -15,6 +15,7 @@ import '../utils/database_helper.dart'; import '../utils/image_path_helper.dart'; import '../utils/user_prefs.dart'; import '../utils/theme/app_theme.dart'; +import '../utils/font_download_manager.dart'; /// 应用全局状态管理 class AppProvider extends ChangeNotifier { @@ -255,6 +256,10 @@ class AppProvider extends ChangeNotifier { _colorSchemeIndex = prefs.colorSchemeIndex; _fontFamily = prefs.fontFamily; AppTheme.setFontFamily(_fontFamily); + // 异步预加载已缓存的字体(不阻塞 UI) + if (_fontFamily.isNotEmpty) { + FontDownloadManager().preloadCachedFont(_fontFamily); + } notifyListeners(); } diff --git a/lib/utils/font_download_manager.dart b/lib/utils/font_download_manager.dart new file mode 100644 index 0000000..368a665 --- /dev/null +++ b/lib/utils/font_download_manager.dart @@ -0,0 +1,174 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +/// 本地字体扫描与加载管理器 +/// +/// 扫描用户指定目录下的字体文件,通过 FontLoader 动态注册到 Flutter。 +class FontDownloadManager { + static final FontDownloadManager _instance = FontDownloadManager._internal(); + factory FontDownloadManager() => _instance; + FontDownloadManager._internal(); + + /// 已加载的字体 family 集合(避免重复注册) + final Set _loadedFonts = {}; + + /// 支持的字体文件扩展名 + static const List _fontExtensions = ['.ttf', '.otf', '.ttc']; + + /// 扫描指定目录下的字体文件 + Future> scanFontDirectory(String dirPath) async { + final dir = Directory(dirPath); + if (!await dir.exists()) { + debugPrint('[FontScan] 目录不存在: $dirPath'); + return []; + } + + final fonts = []; + try { + await for (final entity in dir.list(recursive: true)) { + if (entity is File) { + final ext = path.extension(entity.path).toLowerCase(); + if (_fontExtensions.contains(ext)) { + final fileName = path.basename(entity.path); + fonts.add(FontFileInfo( + path: entity.path, + fileName: fileName, + displayName: _formatFontName(fileName), + )); + } + } + } + } catch (e) { + debugPrint('[FontScan] 扫描异常: $e'); + } + // 按文件名排序 + fonts.sort((a, b) => a.fileName.compareTo(b.fileName)); + debugPrint('[FontScan] 扫描完成: $dirPath, 找到 ${fonts.length} 个字体文件'); + return fonts; + } + + /// 从字体文件名生成显示名称 + String _formatFontName(String fileName) { + // 移除扩展名 + var name = path.basenameWithoutExtension(fileName); + // 替换常见分隔符为空格 + name = name.replaceAll('_', ' ').replaceAll('-', ' '); + // 首字母大写 + return name.split(' ').map((w) { + if (w.isEmpty) return w; + return w[0].toUpperCase() + w.substring(1).toLowerCase(); + }).join(' '); + } + + /// 加载指定字体文件 + /// + /// [filePath] 字体文件完整路径 + /// [family] 可选的字体 family 名称(默认使用文件名) + /// + /// 返回加载成功后的 family 名称 + Future loadFontFile(String filePath, {String? family}) async { + final file = File(filePath); + if (!await file.exists()) return null; + + final fileName = path.basename(filePath); + final familyName = family ?? path.basenameWithoutExtension(fileName); + + // 已加载过,直接返回 + if (_loadedFonts.contains(familyName)) { + return familyName; + } + + try { + final bytes = await file.readAsBytes(); + final loader = FontLoader(familyName); + loader.addFont(Future.value(ByteData.sublistView(bytes))); + await loader.load(); + _loadedFonts.add(familyName); + debugPrint('[FontDownload] 字体加载成功: $familyName'); + return familyName; + } catch (e) { + debugPrint('[FontDownload] 字体加载失败: $familyName, error=$e'); + return null; + } + } + + /// 预加载已缓存的字体(应用启动时调用) + Future preloadCachedFont(String family) async { + if (family.isEmpty) return; + if (_loadedFonts.contains(family)) return; + + // 尝试从默认字体目录加载 + try { + final fontDir = await _getFontDir(); + final file = File(path.join(fontDir.path, '$family.ttf')); + if (await file.exists()) { + await loadFontFile(file.path, family: family); + return; + } + // 尝试其他扩展名 + for (final ext in ['.otf', '.ttc']) { + final file2 = File(path.join(fontDir.path, '$family$ext')); + if (await file2.exists()) { + await loadFontFile(file2.path, family: family); + return; + } + } + } catch (e) { + debugPrint('[FontDownload] 预加载失败: $family, error=$e'); + } + } + + /// 获取字体缓存目录 + Future _getFontDir() async { + if (Platform.isAndroid) { + final fontDir = Directory('/sdcard/Documents/mooknote/fonts'); + if (!await fontDir.exists()) { + await fontDir.create(recursive: true); + } + return fontDir; + } + // iOS / 桌面端 fallback + final appDir = await getApplicationDocumentsDirectory(); + final fontDir = Directory(path.join(appDir.path, 'fonts')); + if (!await fontDir.exists()) { + await fontDir.create(recursive: true); + } + return fontDir; + } + + /// 清理所有下载的字体缓存 + Future clearAllCache() async { + try { + final fontDir = await _getFontDir(); + if (await fontDir.exists()) { + await for (final entity in fontDir.list()) { + if (entity is File) { + try { + await entity.delete(); + } catch (_) {} + } + } + } + _loadedFonts.clear(); + debugPrint('[FontDownload] 字体缓存已清理'); + } catch (e) { + debugPrint('[FontDownload] 清理缓存失败: $e'); + } + } +} + +/// 字体文件信息 +class FontFileInfo { + final String path; + final String fileName; + final String displayName; + + FontFileInfo({ + required this.path, + required this.fileName, + required this.displayName, + }); +} diff --git a/pubspec.lock b/pubspec.lock index dfcdd14..39d93b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + android_intent_plus: + dependency: "direct main" + description: + name: android_intent_plus + sha256: "2329378af63f49b985cb2e110ac784d08374f1e2b1984be77ba9325b1c8cce11" + url: "https://pub.dev" + source: hosted + version: "5.3.1" archive: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5a49eea..457d138 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: flutter_staggered_grid_view: ^0.7.0 http: ^1.2.0 url_launcher: ^6.2.5 + android_intent_plus: ^5.0.0 webview_flutter: ^4.8.0 flutter_inappwebview: ^6.1.5 dynamic_color: ^1.8.1 @@ -62,17 +63,3 @@ flutter: - assets/images/ - assets/icon/ - assets/images/ticket/ - - fonts: - - family: LXGWWenKai - fonts: - - asset: assets/fonts/LXGWWenKai-Regular.ttf - - family: OPPOSans - fonts: - - asset: assets/fonts/OPPO_Sans4.0.ttf - - family: NotoSerifSC - fonts: - - asset: assets/fonts/NotoSerifSC-Regular.ttf - - family: SmileySans - fonts: - - asset: assets/fonts/SmileySans-Oblique.ttf