鸿蒙端提交
This commit is contained in:
@@ -22,13 +22,13 @@ class ImageImportService {
|
||||
final result = await FilePicker.pickFiles(type: FileType.image);
|
||||
if (result == null || result.files.isEmpty) return null;
|
||||
final file = result.files.first;
|
||||
// 优先通过路径读取,避免使用已废弃的 bytes 属性
|
||||
// 优先通过路径读取
|
||||
if (file.path != null) {
|
||||
final f = File(file.path!);
|
||||
return await f.readAsBytes();
|
||||
}
|
||||
// 路径不可用时回退到 readAsBytes
|
||||
return await file.readAsBytes();
|
||||
// 路径不可用时回退到 bytes 属性
|
||||
return file.bytes;
|
||||
} catch (e) {
|
||||
Log.e('图片导入失败', e);
|
||||
return null;
|
||||
|
||||
@@ -75,8 +75,7 @@ class _SpotlightSearchDialog extends ConsumerStatefulWidget {
|
||||
_SpotlightSearchDialogState();
|
||||
}
|
||||
|
||||
class _SpotlightSearchDialogState
|
||||
extends ConsumerState<_SpotlightSearchDialog>
|
||||
class _SpotlightSearchDialogState extends ConsumerState<_SpotlightSearchDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
// ---- 控制器 ----
|
||||
final _searchController = TextEditingController();
|
||||
@@ -113,13 +112,10 @@ class _SpotlightSearchDialogState
|
||||
);
|
||||
|
||||
// 搜索框从上方滑入
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, -0.08),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _entryController,
|
||||
curve: Curves.easeOutCubic,
|
||||
));
|
||||
_slideAnimation =
|
||||
Tween<Offset>(begin: const Offset(0, -0.08), end: Offset.zero).animate(
|
||||
CurvedAnimation(parent: _entryController, curve: Curves.easeOutCubic),
|
||||
);
|
||||
|
||||
// 启动入场动画
|
||||
_entryController.forward();
|
||||
@@ -197,7 +193,9 @@ class _SpotlightSearchDialogState
|
||||
searchState.recentSearches.isNotEmpty)
|
||||
_buildRecentSearches(ext, searchState),
|
||||
if (searchState.results.isNotEmpty)
|
||||
Expanded(child: _buildResults(ext, searchState)),
|
||||
Expanded(
|
||||
child: _buildResults(ext, searchState),
|
||||
),
|
||||
_buildShortcutHints(ext),
|
||||
],
|
||||
),
|
||||
@@ -270,11 +268,7 @@ class _SpotlightSearchDialogState
|
||||
child: Row(
|
||||
children: [
|
||||
// 搜索图标
|
||||
Icon(
|
||||
CupertinoIcons.search,
|
||||
size: 20,
|
||||
color: ext.textHint,
|
||||
),
|
||||
Icon(CupertinoIcons.search, size: 20, color: ext.textHint),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
// 输入框
|
||||
Expanded(
|
||||
@@ -293,9 +287,7 @@ class _SpotlightSearchDialogState
|
||||
vertical: AppSpacing.sm + 2,
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(spotlightSearchProvider.notifier)
|
||||
.updateQuery(value);
|
||||
ref.read(spotlightSearchProvider.notifier).updateQuery(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -303,24 +295,24 @@ class _SpotlightSearchDialogState
|
||||
// 清除按钮
|
||||
if (state.query.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_searchController.clear();
|
||||
ref.read(spotlightSearchProvider.notifier).updateQuery('');
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.xs),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.overlaySubtle.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark,
|
||||
size: 12,
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
)
|
||||
onTap: () {
|
||||
_searchController.clear();
|
||||
ref.read(spotlightSearchProvider.notifier).updateQuery('');
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.xs),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.overlaySubtle.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark,
|
||||
size: 12,
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(duration: 150.ms)
|
||||
.scale(
|
||||
@@ -362,11 +354,7 @@ class _SpotlightSearchDialogState
|
||||
// 标题行
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.clock,
|
||||
size: 14,
|
||||
color: ext.textHint,
|
||||
),
|
||||
Icon(CupertinoIcons.clock, size: 14, color: ext.textHint),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'最近搜索',
|
||||
@@ -384,9 +372,7 @@ class _SpotlightSearchDialogState
|
||||
},
|
||||
child: Text(
|
||||
'清除',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: ext.accent,
|
||||
),
|
||||
style: AppTypography.caption1.copyWith(color: ext.accent),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -411,49 +397,49 @@ class _SpotlightSearchDialogState
|
||||
label: '搜索 $keyword',
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_searchController.text = keyword;
|
||||
ref.read(spotlightSearchProvider.notifier).updateQuery(keyword);
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs + 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary.withValues(alpha: 0.7),
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
border: Border.all(
|
||||
color: ext.overlaySubtle.withValues(alpha: 0.15),
|
||||
width: 0.5,
|
||||
onTap: () {
|
||||
_searchController.text = keyword;
|
||||
ref.read(spotlightSearchProvider.notifier).updateQuery(keyword);
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs + 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary.withValues(alpha: 0.7),
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
border: Border.all(
|
||||
color: ext.overlaySubtle.withValues(alpha: 0.15),
|
||||
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,
|
||||
child: FadeInAnimation(
|
||||
child: switch (entry) {
|
||||
SpotlightCategoryEntry(:final category) =>
|
||||
Semantics(
|
||||
header: true,
|
||||
child: _buildCategoryHeader(ext, category),
|
||||
),
|
||||
SpotlightCategoryEntry(:final category) => Semantics(
|
||||
header: true,
|
||||
child: _buildCategoryHeader(ext, category),
|
||||
),
|
||||
SpotlightItemEntry(:final item) => _buildResultItem(
|
||||
ext,
|
||||
item,
|
||||
state.results.indexOf(item),
|
||||
state.selectedIndex,
|
||||
state.query,
|
||||
),
|
||||
ext,
|
||||
item,
|
||||
state.results.indexOf(item),
|
||||
state.selectedIndex,
|
||||
state.query,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -511,10 +496,7 @@ class _SpotlightSearchDialogState
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
cat.emoji,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
Text(cat.emoji, style: const TextStyle(fontSize: 12)),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
cat.label,
|
||||
@@ -543,90 +525,92 @@ class _SpotlightSearchDialogState
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _confirmAndNavigate(item),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
// 鼠标悬停时更新选中
|
||||
ref.read(spotlightSearchProvider.notifier).selectItem(itemIndex);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: 1,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ext.accent.withValues(alpha: 0.12)
|
||||
: Colors.transparent,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 图标
|
||||
_buildItemIcon(ext, item),
|
||||
const SizedBox(width: AppSpacing.sm + 2),
|
||||
// 文字区域
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 名称(高亮关键词)
|
||||
_buildHighlightedText(ext, item.name, query),
|
||||
// 副标题
|
||||
if (item.subtitle != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 1),
|
||||
child: Text(
|
||||
item.subtitle!,
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _confirmAndNavigate(item),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
// 鼠标悬停时更新选中
|
||||
ref.read(spotlightSearchProvider.notifier).selectItem(itemIndex);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: 1,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ext.accent.withValues(alpha: 0.12)
|
||||
: Colors.transparent,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 图标
|
||||
_buildItemIcon(ext, item),
|
||||
const SizedBox(width: AppSpacing.sm + 2),
|
||||
// 文字区域
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 名称(高亮关键词)
|
||||
_buildHighlightedText(ext, item.name, query),
|
||||
// 副标题
|
||||
if (item.subtitle != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 1),
|
||||
child: Text(
|
||||
item.subtitle!,
|
||||
style: AppTypography.caption2.copyWith(
|
||||
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,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 选中指示箭头
|
||||
if (isSelected) ...[
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Icon(
|
||||
CupertinoIcons.chevron_right,
|
||||
size: 14,
|
||||
color: ext.accent,
|
||||
// 分类标签
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 选中指示箭头
|
||||
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,
|
||||
border: Border.all(
|
||||
color: catColor.withValues(alpha: 0.15),
|
||||
width: 0.5,
|
||||
),
|
||||
border: Border.all(color: catColor.withValues(alpha: 0.15), width: 0.5),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
@@ -744,10 +725,17 @@ class _SpotlightSearchDialogState
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 快捷键提示
|
||||
// 快捷键提示 — 响应式:竖屏2栏 / 横屏1栏
|
||||
// ============================================================
|
||||
|
||||
Widget _buildShortcutHints(AppThemeExtension ext) {
|
||||
final hints = [
|
||||
('↑↓', '导航'),
|
||||
('↵', '打开'),
|
||||
('esc', '关闭'),
|
||||
(io.Platform.isMacOS ? '⌘K' : 'Ctrl+J', '搜索'),
|
||||
];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
@@ -761,17 +749,35 @@ class _SpotlightSearchDialogState
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildHintKey(ext, '↑↓', '导航'),
|
||||
_buildHintSeparator(ext),
|
||||
_buildHintKey(ext, '↵', '打开'),
|
||||
_buildHintSeparator(ext),
|
||||
_buildHintKey(ext, 'esc', '关闭'),
|
||||
_buildHintSeparator(ext),
|
||||
_buildHintKey(ext, io.Platform.isMacOS ? '⌘K' : 'Ctrl+K', '搜索'),
|
||||
],
|
||||
child: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
// 竖屏:2栏(Wrap自动换行,每行约2个)
|
||||
if (orientation == Orientation.portrait) {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: AppSpacing.md,
|
||||
runSpacing: AppSpacing.xs,
|
||||
children: hints
|
||||
.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),
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
),
|
||||
style: AppTypography.caption2.copyWith(color: ext.textHint),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -676,13 +676,14 @@ class FontManagementNotifier extends Notifier<FontManagementState>
|
||||
}
|
||||
|
||||
if (zipBytes == null) {
|
||||
// 使用 readAsBytes 替代已废弃的 bytes 属性
|
||||
zipBytes = await platformFile.readAsBytes();
|
||||
if (zipBytes.isEmpty) {
|
||||
// 使用 bytes 属性读取文件数据
|
||||
final rawBytes = platformFile.bytes;
|
||||
if (rawBytes == null || rawBytes.isEmpty) {
|
||||
Log.w('ZIP字体导入跳过: 无可用文件路径或数据');
|
||||
AppToast.showWarning('无法读取ZIP文件数据');
|
||||
return;
|
||||
}
|
||||
zipBytes = rawBytes;
|
||||
}
|
||||
|
||||
final archive = ZipDecoder().decodeBytes(zipBytes);
|
||||
@@ -696,8 +697,7 @@ class FontManagementNotifier extends Notifier<FontManagementState>
|
||||
if (name.endsWith('.ttf') || name.endsWith('.otf')) {
|
||||
try {
|
||||
final fileName = file.name.split('/').last;
|
||||
final outputPath =
|
||||
'${fontDir.path}/$fileName';
|
||||
final outputPath = '${fontDir.path}/$fileName';
|
||||
await File(outputPath).writeAsBytes(file.content as List<int>);
|
||||
|
||||
final fontFamily = fileName.replaceAll(
|
||||
|
||||
@@ -55,7 +55,8 @@ class FontDownloadService {
|
||||
static bool isValidFontFile(Uint8List bytes) {
|
||||
if (bytes.length < 4) return false;
|
||||
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);
|
||||
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;
|
||||
@@ -86,10 +87,7 @@ class FontDownloadService {
|
||||
ProgressCallback? onProgress,
|
||||
}) async {
|
||||
if (pu.isWeb) {
|
||||
return const FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: 'Web端暂不支持字体下载',
|
||||
);
|
||||
return const FontDownloadResult(success: false, errorMsg: 'Web端暂不支持字体下载');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -104,9 +102,11 @@ class FontDownloadService {
|
||||
bool downloadSuccess = false;
|
||||
String? lastError;
|
||||
|
||||
for (int urlIdx = 0;
|
||||
urlIdx < allUrls.length && !downloadSuccess;
|
||||
urlIdx++) {
|
||||
for (
|
||||
int urlIdx = 0;
|
||||
urlIdx < allUrls.length && !downloadSuccess;
|
||||
urlIdx++
|
||||
) {
|
||||
final currentUrl = allUrls[urlIdx];
|
||||
if (currentUrl.isEmpty) continue;
|
||||
|
||||
@@ -115,9 +115,7 @@ class FontDownloadService {
|
||||
if (attempt > 0) {
|
||||
final delay = Duration(seconds: 1 << (attempt - 1));
|
||||
await Future<void>.delayed(delay);
|
||||
Log.d(
|
||||
'字体下载重试 [$displayName] URL#${urlIdx + 1} 第${attempt + 1}次',
|
||||
);
|
||||
Log.d('字体下载重试 [$displayName] URL#${urlIdx + 1} 第${attempt + 1}次');
|
||||
}
|
||||
|
||||
await _dio.download(
|
||||
@@ -134,9 +132,7 @@ class FontDownloadService {
|
||||
if (await savedFile.exists()) {
|
||||
final bytes = await savedFile.readAsBytes();
|
||||
if (!isValidFontFile(bytes)) {
|
||||
Log.e(
|
||||
'字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式',
|
||||
);
|
||||
Log.e('字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式');
|
||||
await savedFile.delete();
|
||||
lastError = '文件格式无效';
|
||||
continue;
|
||||
@@ -176,10 +172,7 @@ class FontDownloadService {
|
||||
);
|
||||
} catch (e) {
|
||||
Log.e('字体下载异常: $displayName', e);
|
||||
return FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '下载异常: $e',
|
||||
);
|
||||
return FontDownloadResult(success: false, errorMsg: '下载异常: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,10 +182,7 @@ class FontDownloadService {
|
||||
ProgressCallback? onProgress,
|
||||
}) async {
|
||||
if (pu.isWeb) {
|
||||
return const FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: 'Web端暂不支持字体下载',
|
||||
);
|
||||
return const FontDownloadResult(success: false, errorMsg: 'Web端暂不支持字体下载');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -215,8 +205,7 @@ class FontDownloadService {
|
||||
fontFamily = 'CustomFont_${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
final displayName =
|
||||
name?.isNotEmpty == true ? name! : fontFamily;
|
||||
final displayName = name?.isNotEmpty == true ? name! : fontFamily;
|
||||
|
||||
final fileName = '$fontFamily.$ext';
|
||||
final savePath = '${fontDir.path}/$fileName';
|
||||
@@ -246,9 +235,7 @@ class FontDownloadService {
|
||||
if (await savedFile.exists()) {
|
||||
final bytes = await savedFile.readAsBytes();
|
||||
if (!isValidFontFile(bytes)) {
|
||||
Log.e(
|
||||
'URL字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式',
|
||||
);
|
||||
Log.e('URL字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式');
|
||||
await savedFile.delete();
|
||||
lastError = '文件格式无效';
|
||||
continue;
|
||||
@@ -259,14 +246,10 @@ class FontDownloadService {
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
lastError = e.message ?? '网络错误';
|
||||
Log.w(
|
||||
'URL字体下载失败 [$displayName] 第${attempt + 1}次: $lastError',
|
||||
);
|
||||
Log.w('URL字体下载失败 [$displayName] 第${attempt + 1}次: $lastError');
|
||||
} catch (e) {
|
||||
lastError = e.toString();
|
||||
Log.w(
|
||||
'URL字体下载异常 [$displayName] 第${attempt + 1}次: $lastError',
|
||||
);
|
||||
Log.w('URL字体下载异常 [$displayName] 第${attempt + 1}次: $lastError');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,20 +272,14 @@ class FontDownloadService {
|
||||
);
|
||||
} catch (e) {
|
||||
Log.e('URL字体下载异常', e);
|
||||
return FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '下载失败: $e',
|
||||
);
|
||||
return FontDownloadResult(success: false, errorMsg: '下载失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<FontDownloadResult>> importLocalFonts() async {
|
||||
if (pu.isWeb) {
|
||||
return [
|
||||
const FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: 'Web端暂不支持字体导入',
|
||||
),
|
||||
const FontDownloadResult(success: false, errorMsg: 'Web端暂不支持字体导入'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -322,8 +299,7 @@ class FontDownloadService {
|
||||
final fileName = platformFile.name;
|
||||
final name = fileName.replaceAll(RegExp(r'\.(ttf|otf)$'), '');
|
||||
final fontFamily = name.replaceAll(' ', '');
|
||||
final destPath =
|
||||
'${fontDir.path}/$fileName';
|
||||
final destPath = '${fontDir.path}/$fileName';
|
||||
|
||||
Uint8List? fontBytes;
|
||||
|
||||
@@ -340,9 +316,9 @@ class FontDownloadService {
|
||||
}
|
||||
|
||||
if (fontBytes == null) {
|
||||
// 使用 readAsBytes 替代已废弃的 bytes 属性
|
||||
final rawBytes = await platformFile.readAsBytes();
|
||||
if (rawBytes.isNotEmpty) {
|
||||
// 使用 bytes 属性读取文件数据
|
||||
final rawBytes = platformFile.bytes;
|
||||
if (rawBytes != null && rawBytes.isNotEmpty) {
|
||||
fontBytes = rawBytes;
|
||||
if (await File(destPath).exists()) {
|
||||
await File(destPath).delete();
|
||||
@@ -350,59 +326,62 @@ class FontDownloadService {
|
||||
await File(destPath).writeAsBytes(rawBytes);
|
||||
} else {
|
||||
Log.w('字体导入跳过: $fileName — 无可用文件路径或数据');
|
||||
results.add(FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '$name 无法读取文件数据',
|
||||
fontFamily: fontFamily,
|
||||
displayName: name,
|
||||
));
|
||||
results.add(
|
||||
FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '$name 无法读取文件数据',
|
||||
fontFamily: fontFamily,
|
||||
displayName: name,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidFontFile(fontBytes)) {
|
||||
if (!isValidFontFile(fontBytes!)) {
|
||||
if (await File(destPath).exists()) {
|
||||
await File(destPath).delete();
|
||||
}
|
||||
results.add(FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '$name 格式无效',
|
||||
fontFamily: fontFamily,
|
||||
displayName: name,
|
||||
));
|
||||
results.add(
|
||||
FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '$name 格式无效',
|
||||
fontFamily: fontFamily,
|
||||
displayName: name,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
final fileSize = await File(destPath).length();
|
||||
results.add(FontDownloadResult(
|
||||
success: true,
|
||||
savePath: destPath,
|
||||
fileSize: fileSize,
|
||||
fontFamily: fontFamily,
|
||||
displayName: name,
|
||||
));
|
||||
results.add(
|
||||
FontDownloadResult(
|
||||
success: true,
|
||||
savePath: destPath,
|
||||
fileSize: fileSize,
|
||||
fontFamily: fontFamily,
|
||||
displayName: name,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.e('单个字体导入失败: ${platformFile.name}', e);
|
||||
results.add(FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '${platformFile.name} 导入失败: $e',
|
||||
));
|
||||
results.add(
|
||||
FontDownloadResult(
|
||||
success: false,
|
||||
errorMsg: '${platformFile.name} 导入失败: $e',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (e) {
|
||||
Log.e('字体导入失败', e);
|
||||
return [
|
||||
FontDownloadResult(success: false, errorMsg: '导入失败: $e'),
|
||||
];
|
||||
return [FontDownloadResult(success: false, errorMsg: '导入失败: $e')];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> loadFontIntoEngine(
|
||||
String fontFamily,
|
||||
String path,
|
||||
) async {
|
||||
static Future<bool> loadFontIntoEngine(String fontFamily, String path) async {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (!await file.exists()) return false;
|
||||
|
||||
@@ -84,47 +84,52 @@ class LoginGuardWidget extends ConsumerWidget {
|
||||
|
||||
return Semantics(
|
||||
label: '$displayTitle. $displaySubtitle',
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.xxl,
|
||||
),
|
||||
child: GlassContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 48, color: ext.textHint),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
displayTitle,
|
||||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||||
),
|
||||
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,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.xxl,
|
||||
),
|
||||
child: GlassContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 48, color: ext.textHint),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
displayTitle,
|
||||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user