鸿蒙端提交
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user