鸿蒙端提交

This commit is contained in:
Developer
2026-06-07 08:16:20 +08:00
parent ae6804e8bd
commit ff08e6c128
6 changed files with 306 additions and 318 deletions

View File

@@ -22,13 +22,13 @@ class ImageImportService {
final result = await FilePicker.pickFiles(type: FileType.image); final result = await FilePicker.pickFiles(type: FileType.image);
if (result == null || result.files.isEmpty) return null; if (result == null || result.files.isEmpty) return null;
final file = result.files.first; final file = result.files.first;
// 优先通过路径读取,避免使用已废弃的 bytes 属性 // 优先通过路径读取
if (file.path != null) { if (file.path != null) {
final f = File(file.path!); final f = File(file.path!);
return await f.readAsBytes(); return await f.readAsBytes();
} }
// 路径不可用时回退到 readAsBytes // 路径不可用时回退到 bytes 属性
return await file.readAsBytes(); return file.bytes;
} catch (e) { } catch (e) {
Log.e('图片导入失败', e); Log.e('图片导入失败', e);
return null; return null;

View File

@@ -75,8 +75,7 @@ class _SpotlightSearchDialog extends ConsumerStatefulWidget {
_SpotlightSearchDialogState(); _SpotlightSearchDialogState();
} }
class _SpotlightSearchDialogState class _SpotlightSearchDialogState extends ConsumerState<_SpotlightSearchDialog>
extends ConsumerState<_SpotlightSearchDialog>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
// ---- 控制器 ---- // ---- 控制器 ----
final _searchController = TextEditingController(); final _searchController = TextEditingController();
@@ -113,13 +112,10 @@ class _SpotlightSearchDialogState
); );
// 搜索框从上方滑入 // 搜索框从上方滑入
_slideAnimation = Tween<Offset>( _slideAnimation =
begin: const Offset(0, -0.08), Tween<Offset>(begin: const Offset(0, -0.08), end: Offset.zero).animate(
end: Offset.zero, CurvedAnimation(parent: _entryController, curve: Curves.easeOutCubic),
).animate(CurvedAnimation( );
parent: _entryController,
curve: Curves.easeOutCubic,
));
// 启动入场动画 // 启动入场动画
_entryController.forward(); _entryController.forward();
@@ -197,7 +193,9 @@ class _SpotlightSearchDialogState
searchState.recentSearches.isNotEmpty) searchState.recentSearches.isNotEmpty)
_buildRecentSearches(ext, searchState), _buildRecentSearches(ext, searchState),
if (searchState.results.isNotEmpty) if (searchState.results.isNotEmpty)
Expanded(child: _buildResults(ext, searchState)), Expanded(
child: _buildResults(ext, searchState),
),
_buildShortcutHints(ext), _buildShortcutHints(ext),
], ],
), ),
@@ -270,11 +268,7 @@ class _SpotlightSearchDialogState
child: Row( child: Row(
children: [ children: [
// 搜索图标 // 搜索图标
Icon( Icon(CupertinoIcons.search, size: 20, color: ext.textHint),
CupertinoIcons.search,
size: 20,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
// 输入框 // 输入框
Expanded( Expanded(
@@ -293,9 +287,7 @@ class _SpotlightSearchDialogState
vertical: AppSpacing.sm + 2, vertical: AppSpacing.sm + 2,
), ),
onChanged: (value) { onChanged: (value) {
ref ref.read(spotlightSearchProvider.notifier).updateQuery(value);
.read(spotlightSearchProvider.notifier)
.updateQuery(value);
}, },
), ),
), ),
@@ -303,24 +295,24 @@ class _SpotlightSearchDialogState
// 清除按钮 // 清除按钮
if (state.query.isNotEmpty) if (state.query.isNotEmpty)
GestureDetector( GestureDetector(
onTap: () { onTap: () {
_searchController.clear(); _searchController.clear();
ref.read(spotlightSearchProvider.notifier).updateQuery(''); ref.read(spotlightSearchProvider.notifier).updateQuery('');
_focusNode.requestFocus(); _focusNode.requestFocus();
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(AppSpacing.xs), padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ext.overlaySubtle.withValues(alpha: 0.3), color: ext.overlaySubtle.withValues(alpha: 0.3),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
CupertinoIcons.xmark, CupertinoIcons.xmark,
size: 12, size: 12,
color: ext.textSecondary, color: ext.textSecondary,
), ),
), ),
) )
.animate() .animate()
.fadeIn(duration: 150.ms) .fadeIn(duration: 150.ms)
.scale( .scale(
@@ -362,11 +354,7 @@ class _SpotlightSearchDialogState
// 标题行 // 标题行
Row( Row(
children: [ children: [
Icon( Icon(CupertinoIcons.clock, size: 14, color: ext.textHint),
CupertinoIcons.clock,
size: 14,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.xs), const SizedBox(width: AppSpacing.xs),
Text( Text(
'最近搜索', '最近搜索',
@@ -384,9 +372,7 @@ class _SpotlightSearchDialogState
}, },
child: Text( child: Text(
'清除', '清除',
style: AppTypography.caption1.copyWith( style: AppTypography.caption1.copyWith(color: ext.accent),
color: ext.accent,
),
), ),
), ),
], ],
@@ -411,49 +397,49 @@ class _SpotlightSearchDialogState
label: '搜索 $keyword', label: '搜索 $keyword',
button: true, button: true,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
_searchController.text = keyword; _searchController.text = keyword;
ref.read(spotlightSearchProvider.notifier).updateQuery(keyword); ref.read(spotlightSearchProvider.notifier).updateQuery(keyword);
_focusNode.requestFocus(); _focusNode.requestFocus();
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm, horizontal: AppSpacing.sm,
vertical: AppSpacing.xs + 1, vertical: AppSpacing.xs + 1,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ext.bgSecondary.withValues(alpha: 0.7), color: ext.bgSecondary.withValues(alpha: 0.7),
borderRadius: AppRadius.pillBorder, borderRadius: AppRadius.pillBorder,
border: Border.all( border: Border.all(
color: ext.overlaySubtle.withValues(alpha: 0.15), color: ext.overlaySubtle.withValues(alpha: 0.15),
width: 0.5, width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
keyword,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
const SizedBox(width: 2),
GestureDetector(
onTap: () {
ref
.read(spotlightSearchProvider.notifier)
.removeRecentSearch(keyword);
},
child: Icon(
CupertinoIcons.xmark,
size: 10,
color: ext.textHint,
),
),
],
), ),
), ),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
keyword,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
const SizedBox(width: 2),
GestureDetector(
onTap: () {
ref
.read(spotlightSearchProvider.notifier)
.removeRecentSearch(keyword);
},
child: Icon(
CupertinoIcons.xmark,
size: 10,
color: ext.textHint,
),
),
],
),
),
), ),
); );
} }
@@ -479,18 +465,17 @@ class _SpotlightSearchDialogState
verticalOffset: 12.0, verticalOffset: 12.0,
child: FadeInAnimation( child: FadeInAnimation(
child: switch (entry) { child: switch (entry) {
SpotlightCategoryEntry(:final category) => SpotlightCategoryEntry(:final category) => Semantics(
Semantics( header: true,
header: true, child: _buildCategoryHeader(ext, category),
child: _buildCategoryHeader(ext, category), ),
),
SpotlightItemEntry(:final item) => _buildResultItem( SpotlightItemEntry(:final item) => _buildResultItem(
ext, ext,
item, item,
state.results.indexOf(item), state.results.indexOf(item),
state.selectedIndex, state.selectedIndex,
state.query, state.query,
), ),
}, },
), ),
), ),
@@ -511,10 +496,7 @@ class _SpotlightSearchDialogState
), ),
child: Row( child: Row(
children: [ children: [
Text( Text(cat.emoji, style: const TextStyle(fontSize: 12)),
cat.emoji,
style: const TextStyle(fontSize: 12),
),
const SizedBox(width: AppSpacing.xs), const SizedBox(width: AppSpacing.xs),
Text( Text(
cat.label, cat.label,
@@ -543,90 +525,92 @@ class _SpotlightSearchDialogState
button: true, button: true,
selected: isSelected, selected: isSelected,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => _confirmAndNavigate(item), onTap: () => _confirmAndNavigate(item),
child: MouseRegion( child: MouseRegion(
onEnter: (_) { onEnter: (_) {
// 鼠标悬停时更新选中 // 鼠标悬停时更新选中
ref.read(spotlightSearchProvider.notifier).selectItem(itemIndex); ref.read(spotlightSearchProvider.notifier).selectItem(itemIndex);
}, },
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm, horizontal: AppSpacing.sm,
vertical: 1, vertical: 1,
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm, horizontal: AppSpacing.sm,
vertical: AppSpacing.sm, vertical: AppSpacing.sm,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? ext.accent.withValues(alpha: 0.12) ? ext.accent.withValues(alpha: 0.12)
: Colors.transparent, : Colors.transparent,
borderRadius: AppRadius.mdBorder, borderRadius: AppRadius.mdBorder,
), ),
child: Row( child: Row(
children: [ children: [
// 图标 // 图标
_buildItemIcon(ext, item), _buildItemIcon(ext, item),
const SizedBox(width: AppSpacing.sm + 2), const SizedBox(width: AppSpacing.sm + 2),
// 文字区域 // 文字区域
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 名称(高亮关键词) // 名称(高亮关键词)
_buildHighlightedText(ext, item.name, query), _buildHighlightedText(ext, item.name, query),
// 副标题 // 副标题
if (item.subtitle != null) if (item.subtitle != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 1), padding: const EdgeInsets.only(top: 1),
child: Text( child: Text(
item.subtitle!, item.subtitle!,
style: AppTypography.caption2.copyWith( style: AppTypography.caption2.copyWith(
color: ext.textHint, color: ext.textHint,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
), ],
],
),
),
// 分类标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm - 2,
vertical: 2,
),
decoration: BoxDecoration(
color: _categoryColor(item.category).withValues(alpha: 0.12),
borderRadius: AppRadius.pillBorder,
),
child: Text(
item.category.label,
style: AppTypography.caption2.copyWith(
color: _categoryColor(item.category),
fontWeight: FontWeight.w500,
), ),
), ),
), // 分类标签
// 选中指示箭头 Container(
if (isSelected) ...[ padding: const EdgeInsets.symmetric(
const SizedBox(width: AppSpacing.xs), horizontal: AppSpacing.sm - 2,
Icon( vertical: 2,
CupertinoIcons.chevron_right, ),
size: 14, decoration: BoxDecoration(
color: ext.accent, color: _categoryColor(
item.category,
).withValues(alpha: 0.12),
borderRadius: AppRadius.pillBorder,
),
child: Text(
item.category.label,
style: AppTypography.caption2.copyWith(
color: _categoryColor(item.category),
fontWeight: FontWeight.w500,
),
),
), ),
// 选中指示箭头
if (isSelected) ...[
const SizedBox(width: AppSpacing.xs),
Icon(
CupertinoIcons.chevron_right,
size: 14,
color: ext.accent,
),
],
], ],
], ),
), ),
), ),
), ),
),
); );
} }
@@ -647,10 +631,7 @@ class _SpotlightSearchDialogState
], ],
), ),
borderRadius: AppRadius.mdBorder, borderRadius: AppRadius.mdBorder,
border: Border.all( border: Border.all(color: catColor.withValues(alpha: 0.15), width: 0.5),
color: catColor.withValues(alpha: 0.15),
width: 0.5,
),
), ),
child: Center( child: Center(
child: Text( child: Text(
@@ -744,10 +725,17 @@ class _SpotlightSearchDialogState
} }
// ============================================================ // ============================================================
// 快捷键提示 // 快捷键提示 — 响应式竖屏2栏 / 横屏1栏
// ============================================================ // ============================================================
Widget _buildShortcutHints(AppThemeExtension ext) { Widget _buildShortcutHints(AppThemeExtension ext) {
final hints = [
('↑↓', '导航'),
('', '打开'),
('esc', '关闭'),
(io.Platform.isMacOS ? '⌘K' : 'Ctrl+J', '搜索'),
];
return Container( return Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md, horizontal: AppSpacing.md,
@@ -761,17 +749,35 @@ class _SpotlightSearchDialogState
), ),
), ),
), ),
child: Row( child: OrientationBuilder(
mainAxisAlignment: MainAxisAlignment.center, builder: (context, orientation) {
children: [ // 竖屏2栏Wrap自动换行每行约2个
_buildHintKey(ext, '↑↓', '导航'), if (orientation == Orientation.portrait) {
_buildHintSeparator(ext), return Wrap(
_buildHintKey(ext, '', '打开'), alignment: WrapAlignment.center,
_buildHintSeparator(ext), spacing: AppSpacing.md,
_buildHintKey(ext, 'esc', '关闭'), runSpacing: AppSpacing.xs,
_buildHintSeparator(ext), children: hints
_buildHintKey(ext, io.Platform.isMacOS ? '⌘K' : 'Ctrl+K', '搜索'), .map((h) => _buildHintKey(ext, h.$1, h.$2))
], .toList(),
);
}
// 横屏1栏单行
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...hints
.expand(
(h) => [
_buildHintKey(ext, h.$1, h.$2),
_buildHintSeparator(ext),
],
)
.toList()
..removeLast(),
],
);
},
), ),
); );
} }
@@ -805,9 +811,7 @@ class _SpotlightSearchDialogState
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
label, label,
style: AppTypography.caption2.copyWith( style: AppTypography.caption2.copyWith(color: ext.textHint),
color: ext.textHint,
),
), ),
], ],
); );

View File

@@ -676,13 +676,14 @@ class FontManagementNotifier extends Notifier<FontManagementState>
} }
if (zipBytes == null) { if (zipBytes == null) {
// 使用 readAsBytes 替代已废弃的 bytes 属性 // 使用 bytes 属性读取文件数据
zipBytes = await platformFile.readAsBytes(); final rawBytes = platformFile.bytes;
if (zipBytes.isEmpty) { if (rawBytes == null || rawBytes.isEmpty) {
Log.w('ZIP字体导入跳过: 无可用文件路径或数据'); Log.w('ZIP字体导入跳过: 无可用文件路径或数据');
AppToast.showWarning('无法读取ZIP文件数据'); AppToast.showWarning('无法读取ZIP文件数据');
return; return;
} }
zipBytes = rawBytes;
} }
final archive = ZipDecoder().decodeBytes(zipBytes); final archive = ZipDecoder().decodeBytes(zipBytes);
@@ -696,8 +697,7 @@ class FontManagementNotifier extends Notifier<FontManagementState>
if (name.endsWith('.ttf') || name.endsWith('.otf')) { if (name.endsWith('.ttf') || name.endsWith('.otf')) {
try { try {
final fileName = file.name.split('/').last; final fileName = file.name.split('/').last;
final outputPath = final outputPath = '${fontDir.path}/$fileName';
'${fontDir.path}/$fileName';
await File(outputPath).writeAsBytes(file.content as List<int>); await File(outputPath).writeAsBytes(file.content as List<int>);
final fontFamily = fileName.replaceAll( final fontFamily = fileName.replaceAll(

View File

@@ -55,7 +55,8 @@ class FontDownloadService {
static bool isValidFontFile(Uint8List bytes) { static bool isValidFontFile(Uint8List bytes) {
if (bytes.length < 4) return false; if (bytes.length < 4) return false;
final h = bytes; final h = bytes;
final isTtf = (h[0] == 0x00 && h[1] == 0x01 && h[2] == 0x00 && h[3] == 0x00) || final isTtf =
(h[0] == 0x00 && h[1] == 0x01 && h[2] == 0x00 && h[3] == 0x00) ||
(h[0] == 0x74 && h[1] == 0x72 && h[2] == 0x75 && h[3] == 0x65); (h[0] == 0x74 && h[1] == 0x72 && h[2] == 0x75 && h[3] == 0x65);
final isOtf = h[0] == 0x4F && h[1] == 0x54 && h[2] == 0x54 && h[3] == 0x4F; final isOtf = h[0] == 0x4F && h[1] == 0x54 && h[2] == 0x54 && h[3] == 0x4F;
final isTtc = h[0] == 0x74 && h[1] == 0x74 && h[2] == 0x63 && h[3] == 0x66; final isTtc = h[0] == 0x74 && h[1] == 0x74 && h[2] == 0x63 && h[3] == 0x66;
@@ -86,10 +87,7 @@ class FontDownloadService {
ProgressCallback? onProgress, ProgressCallback? onProgress,
}) async { }) async {
if (pu.isWeb) { if (pu.isWeb) {
return const FontDownloadResult( return const FontDownloadResult(success: false, errorMsg: 'Web端暂不支持字体下载');
success: false,
errorMsg: 'Web端暂不支持字体下载',
);
} }
try { try {
@@ -104,9 +102,11 @@ class FontDownloadService {
bool downloadSuccess = false; bool downloadSuccess = false;
String? lastError; String? lastError;
for (int urlIdx = 0; for (
urlIdx < allUrls.length && !downloadSuccess; int urlIdx = 0;
urlIdx++) { urlIdx < allUrls.length && !downloadSuccess;
urlIdx++
) {
final currentUrl = allUrls[urlIdx]; final currentUrl = allUrls[urlIdx];
if (currentUrl.isEmpty) continue; if (currentUrl.isEmpty) continue;
@@ -115,9 +115,7 @@ class FontDownloadService {
if (attempt > 0) { if (attempt > 0) {
final delay = Duration(seconds: 1 << (attempt - 1)); final delay = Duration(seconds: 1 << (attempt - 1));
await Future<void>.delayed(delay); await Future<void>.delayed(delay);
Log.d( Log.d('字体下载重试 [$displayName] URL#${urlIdx + 1}${attempt + 1}');
'字体下载重试 [$displayName] URL#${urlIdx + 1}${attempt + 1}',
);
} }
await _dio.download( await _dio.download(
@@ -134,9 +132,7 @@ class FontDownloadService {
if (await savedFile.exists()) { if (await savedFile.exists()) {
final bytes = await savedFile.readAsBytes(); final bytes = await savedFile.readAsBytes();
if (!isValidFontFile(bytes)) { if (!isValidFontFile(bytes)) {
Log.e( Log.e('字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式');
'字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式',
);
await savedFile.delete(); await savedFile.delete();
lastError = '文件格式无效'; lastError = '文件格式无效';
continue; continue;
@@ -176,10 +172,7 @@ class FontDownloadService {
); );
} catch (e) { } catch (e) {
Log.e('字体下载异常: $displayName', e); Log.e('字体下载异常: $displayName', e);
return FontDownloadResult( return FontDownloadResult(success: false, errorMsg: '下载异常: $e');
success: false,
errorMsg: '下载异常: $e',
);
} }
} }
@@ -189,10 +182,7 @@ class FontDownloadService {
ProgressCallback? onProgress, ProgressCallback? onProgress,
}) async { }) async {
if (pu.isWeb) { if (pu.isWeb) {
return const FontDownloadResult( return const FontDownloadResult(success: false, errorMsg: 'Web端暂不支持字体下载');
success: false,
errorMsg: 'Web端暂不支持字体下载',
);
} }
try { try {
@@ -215,8 +205,7 @@ class FontDownloadService {
fontFamily = 'CustomFont_${DateTime.now().millisecondsSinceEpoch}'; fontFamily = 'CustomFont_${DateTime.now().millisecondsSinceEpoch}';
} }
final displayName = final displayName = name?.isNotEmpty == true ? name! : fontFamily;
name?.isNotEmpty == true ? name! : fontFamily;
final fileName = '$fontFamily.$ext'; final fileName = '$fontFamily.$ext';
final savePath = '${fontDir.path}/$fileName'; final savePath = '${fontDir.path}/$fileName';
@@ -246,9 +235,7 @@ class FontDownloadService {
if (await savedFile.exists()) { if (await savedFile.exists()) {
final bytes = await savedFile.readAsBytes(); final bytes = await savedFile.readAsBytes();
if (!isValidFontFile(bytes)) { if (!isValidFontFile(bytes)) {
Log.e( Log.e('URL字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式');
'URL字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式',
);
await savedFile.delete(); await savedFile.delete();
lastError = '文件格式无效'; lastError = '文件格式无效';
continue; continue;
@@ -259,14 +246,10 @@ class FontDownloadService {
} }
} on DioException catch (e) { } on DioException catch (e) {
lastError = e.message ?? '网络错误'; lastError = e.message ?? '网络错误';
Log.w( Log.w('URL字体下载失败 [$displayName] 第${attempt + 1}次: $lastError');
'URL字体下载失败 [$displayName] 第${attempt + 1}次: $lastError',
);
} catch (e) { } catch (e) {
lastError = e.toString(); lastError = e.toString();
Log.w( Log.w('URL字体下载异常 [$displayName] 第${attempt + 1}次: $lastError');
'URL字体下载异常 [$displayName] 第${attempt + 1}次: $lastError',
);
} }
} }
@@ -289,20 +272,14 @@ class FontDownloadService {
); );
} catch (e) { } catch (e) {
Log.e('URL字体下载异常', e); Log.e('URL字体下载异常', e);
return FontDownloadResult( return FontDownloadResult(success: false, errorMsg: '下载失败: $e');
success: false,
errorMsg: '下载失败: $e',
);
} }
} }
static Future<List<FontDownloadResult>> importLocalFonts() async { static Future<List<FontDownloadResult>> importLocalFonts() async {
if (pu.isWeb) { if (pu.isWeb) {
return [ return [
const FontDownloadResult( const FontDownloadResult(success: false, errorMsg: 'Web端暂不支持字体导入'),
success: false,
errorMsg: 'Web端暂不支持字体导入',
),
]; ];
} }
@@ -322,8 +299,7 @@ class FontDownloadService {
final fileName = platformFile.name; final fileName = platformFile.name;
final name = fileName.replaceAll(RegExp(r'\.(ttf|otf)$'), ''); final name = fileName.replaceAll(RegExp(r'\.(ttf|otf)$'), '');
final fontFamily = name.replaceAll(' ', ''); final fontFamily = name.replaceAll(' ', '');
final destPath = final destPath = '${fontDir.path}/$fileName';
'${fontDir.path}/$fileName';
Uint8List? fontBytes; Uint8List? fontBytes;
@@ -340,9 +316,9 @@ class FontDownloadService {
} }
if (fontBytes == null) { if (fontBytes == null) {
// 使用 readAsBytes 替代已废弃的 bytes 属性 // 使用 bytes 属性读取文件数据
final rawBytes = await platformFile.readAsBytes(); final rawBytes = platformFile.bytes;
if (rawBytes.isNotEmpty) { if (rawBytes != null && rawBytes.isNotEmpty) {
fontBytes = rawBytes; fontBytes = rawBytes;
if (await File(destPath).exists()) { if (await File(destPath).exists()) {
await File(destPath).delete(); await File(destPath).delete();
@@ -350,59 +326,62 @@ class FontDownloadService {
await File(destPath).writeAsBytes(rawBytes); await File(destPath).writeAsBytes(rawBytes);
} else { } else {
Log.w('字体导入跳过: $fileName — 无可用文件路径或数据'); Log.w('字体导入跳过: $fileName — 无可用文件路径或数据');
results.add(FontDownloadResult( results.add(
success: false, FontDownloadResult(
errorMsg: '$name 无法读取文件数据', success: false,
fontFamily: fontFamily, errorMsg: '$name 无法读取文件数据',
displayName: name, fontFamily: fontFamily,
)); displayName: name,
),
);
continue; continue;
} }
} }
if (!isValidFontFile(fontBytes)) { if (!isValidFontFile(fontBytes!)) {
if (await File(destPath).exists()) { if (await File(destPath).exists()) {
await File(destPath).delete(); await File(destPath).delete();
} }
results.add(FontDownloadResult( results.add(
success: false, FontDownloadResult(
errorMsg: '$name 格式无效', success: false,
fontFamily: fontFamily, errorMsg: '$name 格式无效',
displayName: name, fontFamily: fontFamily,
)); displayName: name,
),
);
continue; continue;
} }
final fileSize = await File(destPath).length(); final fileSize = await File(destPath).length();
results.add(FontDownloadResult( results.add(
success: true, FontDownloadResult(
savePath: destPath, success: true,
fileSize: fileSize, savePath: destPath,
fontFamily: fontFamily, fileSize: fileSize,
displayName: name, fontFamily: fontFamily,
)); displayName: name,
),
);
} catch (e) { } catch (e) {
Log.e('单个字体导入失败: ${platformFile.name}', e); Log.e('单个字体导入失败: ${platformFile.name}', e);
results.add(FontDownloadResult( results.add(
success: false, FontDownloadResult(
errorMsg: '${platformFile.name} 导入失败: $e', success: false,
)); errorMsg: '${platformFile.name} 导入失败: $e',
),
);
} }
} }
return results; return results;
} catch (e) { } catch (e) {
Log.e('字体导入失败', e); Log.e('字体导入失败', e);
return [ return [FontDownloadResult(success: false, errorMsg: '导入失败: $e')];
FontDownloadResult(success: false, errorMsg: '导入失败: $e'),
];
} }
} }
static Future<bool> loadFontIntoEngine( static Future<bool> loadFontIntoEngine(String fontFamily, String path) async {
String fontFamily,
String path,
) async {
try { try {
final file = File(path); final file = File(path);
if (!await file.exists()) return false; if (!await file.exists()) return false;

View File

@@ -84,47 +84,52 @@ class LoginGuardWidget extends ConsumerWidget {
return Semantics( return Semantics(
label: '$displayTitle. $displaySubtitle', label: '$displayTitle. $displaySubtitle',
child: Padding( child: Center(
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: AppSpacing.md, padding: const EdgeInsets.symmetric(
vertical: AppSpacing.xxl, horizontal: AppSpacing.lg,
), vertical: AppSpacing.xxl,
child: GlassContainer( ),
child: Column( child: GlassContainer(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Icon(icon, size: 48, color: ext.textHint), crossAxisAlignment: CrossAxisAlignment.center,
const SizedBox(height: AppSpacing.md), children: [
Text( Icon(icon, size: 48, color: ext.textHint),
displayTitle, const SizedBox(height: AppSpacing.md),
style: AppTypography.title3.copyWith(color: ext.textPrimary), Text(
), displayTitle,
const SizedBox(height: AppSpacing.sm), style: AppTypography.title3.copyWith(color: ext.textPrimary),
Text(
displaySubtitle,
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
color: ext.accent,
borderRadius: AppRadius.pillBorder,
onPressed: () {
context.appPush(AppRoutes.login);
onLogin?.call();
},
child: Text(
displayButtonText,
style: AppTypography.body.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
), ),
), const SizedBox(height: AppSpacing.sm),
Text(
displaySubtitle,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
color: ext.accent,
borderRadius: AppRadius.pillBorder,
onPressed: () {
context.appPush(AppRoutes.login);
onLogin?.call();
},
child: Text(
displayButtonText,
style: AppTypography.body.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
),
),
),
],
), ),
], ),
), ),
), ),
),
); );
} }
} }

View File

@@ -17,7 +17,7 @@ import flutter_app_group_directory
import flutter_image_compress_macos import flutter_image_compress_macos
import flutter_inappwebview_macos import flutter_inappwebview_macos
import flutter_local_notifications import flutter_local_notifications
import flutter_secure_storage_darwin import flutter_secure_storage_macos
import flutter_tts import flutter_tts
import flutter_webrtc import flutter_webrtc
import gal import gal
@@ -55,7 +55,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))