鸿蒙端提交

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);
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;

View File

@@ -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),
),
],
);

View File

@@ -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(

View File

@@ -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;

View File

@@ -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,
),
),
),
],
),
],
),
),
),
),
);
}
}