降低安装包体积,更改问本地选择字体

This commit is contained in:
DelLevin-Home
2026-07-03 15:01:05 +08:00
parent a39c93cda3
commit f171b431d3
10 changed files with 672 additions and 116 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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<FontPickerPage> createState() => _FontPickerPageState();
}
class _FontPickerPageState extends State<FontPickerPage> {
final TextEditingController _pathController = TextEditingController();
final FontDownloadManager _fontManager = FontDownloadManager();
List<FontFileInfo> _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<bool> _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<void> _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<void> _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<void> _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<void> _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),
);
}
}

View File

@@ -7,6 +7,7 @@ import 'package:path/path.dart' as path;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:permission_handler/permission_handler.dart';
import '../main.dart' show routeObserver; import '../main.dart' show routeObserver;
import '../models/data_models.dart'; import '../models/data_models.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
@@ -25,6 +26,7 @@ import 'sync/cloud_sync_page.dart';
import 'app_icon_picker_page.dart'; import 'app_icon_picker_page.dart';
import 'tag_management_page.dart'; import 'tag_management_page.dart';
import 'stroll_page.dart'; import 'stroll_page.dart';
import 'font_picker_page.dart';
/// 个人中心页面 /// 个人中心页面
class ProfilePage extends StatefulWidget { class ProfilePage extends StatefulWidget {
@@ -680,11 +682,7 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
() => Navigator.push( () => Navigator.push(
context, MaterialPageRoute(builder: (_) => const SettingsPage())) context, MaterialPageRoute(builder: (_) => const SettingsPage()))
), ),
( (Icons.feedback_outlined, '反馈', () => _showFeedbackDialog(context)),
Icons.feedback_outlined,
'反馈',
() => _showFeedbackDialog(context)
),
]; ];
return Padding( return Padding(
@@ -808,7 +806,8 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
Text('作者邮箱', Text('作者邮箱',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.5))), color:
colors.onSurface.withValues(alpha: 0.5))),
const SizedBox(height: 2), const SizedBox(height: 2),
Text(email, Text(email,
style: TextStyle( style: TextStyle(
@@ -824,7 +823,8 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
ToastUtil.show(context, '已复制到剪贴板'); ToastUtil.show(context, '已复制到剪贴板');
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.primary.withValues(alpha: 0.08), color: colors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -834,7 +834,11 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
children: [ children: [
Icon(Icons.copy, size: 14, color: colors.primary), Icon(Icons.copy, size: 14, color: colors.primary),
const SizedBox(width: 4), 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<ProfilePage> with RouteAware {
Text('QQ 群', Text('QQ 群',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.5))), color:
colors.onSurface.withValues(alpha: 0.5))),
const SizedBox(height: 2), const SizedBox(height: 2),
Text('1087203310', Text('1087203310',
style: TextStyle( style: TextStyle(
@@ -880,7 +885,8 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
ToastUtil.show(context, '已复制到剪贴板'); ToastUtil.show(context, '已复制到剪贴板');
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.primary.withValues(alpha: 0.08), color: colors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -890,7 +896,11 @@ class _ProfilePageState extends State<ProfilePage> with RouteAware {
children: [ children: [
Icon(Icons.copy, size: 14, color: colors.primary), Icon(Icons.copy, size: 14, color: colors.primary),
const SizedBox(width: 4), 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<SettingsPage> {
subtitle: '清理未在数据库中引用的文件', subtitle: '清理未在数据库中引用的文件',
onTap: () => _showClearCacheDialog(context), onTap: () => _showClearCacheDialog(context),
), ),
Divider(
height: 0.5,
indent: 24,
endIndent: 24,
color: colors.outlineVariant),
_buildActionItem(
icon: Icons.folder_outlined,
title: '获取系统权限',
subtitle: '前往系统设置开启存储权限',
onTap: _showStoragePermissionDialog,
),
Divider( Divider(
height: 0.5, height: 0.5,
indent: 24, indent: 24,
@@ -1758,28 +1779,20 @@ class _SettingsPageState extends State<SettingsPage> {
// ─── 字体选择器 ─── // ─── 字体选择器 ───
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() { Widget _buildFontSelector() {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
final idx = _fontValues.indexOf(_fontFamily); final label = _fontFamily.isEmpty ? '系统默认' : _fontFamily;
final label = idx >= 0 ? _fontLabels[idx] : '系统默认';
return InkWell( return InkWell(
onTap: _showFontPicker, onTap: () async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (_) => const FontPickerPage(initialFamily: '')),
);
if (result != null && mounted) {
_setFontFamily(result);
}
},
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row(children: [ child: Row(children: [
@@ -1814,76 +1827,6 @@ class _SettingsPageState extends State<SettingsPage> {
); );
} }
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) { void _setFontFamily(String family) {
setState(() => _fontFamily = family); setState(() => _fontFamily = family);
context.read<AppProvider>().setFontFamily(family); context.read<AppProvider>().setFontFamily(family);
@@ -2040,6 +1983,46 @@ class _SettingsPageState extends State<SettingsPage> {
); );
} }
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) { void _showClearCacheDialog(BuildContext pageContext) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
showDialog( showDialog(
@@ -2174,8 +2157,8 @@ class _SettingsPageState extends State<SettingsPage> {
try { try {
final db = await DatabaseHelper.instance.database; final db = await DatabaseHelper.instance.database;
// 收集数据库中所有引用的 epub_books 子目录名(包括软删除的) // 收集数据库中所有引用的 epub_books 子目录名(包括软删除的)
final rows = await db final rows = await db.query('reader_books',
.query('reader_books', columns: ['id', 'file_path', 'cover_path', 'is_deleted']); columns: ['id', 'file_path', 'cover_path', 'is_deleted']);
final usedDirs = <String>{}; final usedDirs = <String>{};
for (final r in rows) { for (final r in rows) {
// 只收集未删除的记录对应的目录 // 只收集未删除的记录对应的目录

View File

@@ -15,6 +15,7 @@ import '../utils/database_helper.dart';
import '../utils/image_path_helper.dart'; import '../utils/image_path_helper.dart';
import '../utils/user_prefs.dart'; import '../utils/user_prefs.dart';
import '../utils/theme/app_theme.dart'; import '../utils/theme/app_theme.dart';
import '../utils/font_download_manager.dart';
/// 应用全局状态管理 /// 应用全局状态管理
class AppProvider extends ChangeNotifier { class AppProvider extends ChangeNotifier {
@@ -255,6 +256,10 @@ class AppProvider extends ChangeNotifier {
_colorSchemeIndex = prefs.colorSchemeIndex; _colorSchemeIndex = prefs.colorSchemeIndex;
_fontFamily = prefs.fontFamily; _fontFamily = prefs.fontFamily;
AppTheme.setFontFamily(_fontFamily); AppTheme.setFontFamily(_fontFamily);
// 异步预加载已缓存的字体(不阻塞 UI
if (_fontFamily.isNotEmpty) {
FontDownloadManager().preloadCachedFont(_fontFamily);
}
notifyListeners(); notifyListeners();
} }

View File

@@ -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<String> _loadedFonts = {};
/// 支持的字体文件扩展名
static const List<String> _fontExtensions = ['.ttf', '.otf', '.ttc'];
/// 扫描指定目录下的字体文件
Future<List<FontFileInfo>> scanFontDirectory(String dirPath) async {
final dir = Directory(dirPath);
if (!await dir.exists()) {
debugPrint('[FontScan] 目录不存在: $dirPath');
return [];
}
final fonts = <FontFileInfo>[];
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<String?> 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<void> 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<Directory> _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<void> 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,
});
}

View File

@@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: 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: archive:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -30,6 +30,7 @@ dependencies:
flutter_staggered_grid_view: ^0.7.0 flutter_staggered_grid_view: ^0.7.0
http: ^1.2.0 http: ^1.2.0
url_launcher: ^6.2.5 url_launcher: ^6.2.5
android_intent_plus: ^5.0.0
webview_flutter: ^4.8.0 webview_flutter: ^4.8.0
flutter_inappwebview: ^6.1.5 flutter_inappwebview: ^6.1.5
dynamic_color: ^1.8.1 dynamic_color: ^1.8.1
@@ -62,17 +63,3 @@ flutter:
- assets/images/ - assets/images/
- assets/icon/ - assets/icon/
- assets/images/ticket/ - 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