Files
xianyan/lib/features/template/wallpaper_gallery/wallpaper_source_bar.dart
Developer 83720002e6 feat: 新增工作台模式、系统托盘,修复多平台兼容性问题
1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
2026-06-19 06:43:55 +08:00

290 lines
9.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 壁纸源切换横滑组件
/// 创建时间: 2026-05-04
/// 更新时间: 2026-06-19
/// 作用: 横向滑动切换壁纸来源,显示源名称+速度标签+健康状态
/// + 收藏/全部/已缓存(离线模式) 三个快捷入口
/// 上次更新: 新增"已缓存"离线模式切换chip支持离线浏览已缓存壁纸
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_radius.dart';
import '../../../editor/services/core/editor_icons.dart';
import '../models/template_models.dart';
import '../services/wallpaper_health_service.dart';
class WallpaperSourceBar extends StatelessWidget {
const WallpaperSourceBar({
super.key,
required this.currentSource,
required this.onSourceChanged,
this.showSpeed = true,
this.isFavoriteMode = false,
this.onFavoriteTap,
this.isOfflineMode = false,
this.onOfflineTap,
this.cachedCount = 0,
});
final WallpaperSource? currentSource;
final ValueChanged<WallpaperSource?> onSourceChanged;
final bool showSpeed;
final bool isFavoriteMode;
final VoidCallback? onFavoriteTap;
/// 是否处于离线缓存模式
final bool isOfflineMode;
/// 离线模式切换回调
final VoidCallback? onOfflineTap;
/// 已缓存壁纸数量用于在chip上显示
final int cachedCount;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final isAll = currentSource == null &&
!isFavoriteMode &&
!isOfflineMode;
final healthMap = WallpaperHealthService.healthMap;
return SizedBox(
height: 40,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
itemCount: WallpaperSource.values.length + 3,
separatorBuilder: (_, __) => const SizedBox(width: 6),
itemBuilder: (_, index) {
if (index == 0) {
return _SourceChip(
label: '⭐ 收藏',
isActive: isFavoriteMode,
ext: ext,
showSpeed: false,
speedText: '',
healthStatus: _HealthStatus.normal,
onTap: () => onFavoriteTap?.call(),
);
}
if (index == 1) {
return _SourceChip(
label: '🌐 全部',
isActive: isAll,
ext: ext,
showSpeed: false,
speedText: '',
healthStatus: _HealthStatus.normal,
onTap: () => onSourceChanged(null),
);
}
if (index == 2) {
return _SourceChip(
label: cachedCount > 0 ? '💾 已缓存 $cachedCount' : '💾 已缓存',
isActive: isOfflineMode,
ext: ext,
showSpeed: false,
speedText: '',
healthStatus: _HealthStatus.normal,
onTap: () => onOfflineTap?.call(),
);
}
final source = WallpaperSource.values[index - 3];
final isActive = source == currentSource &&
!isFavoriteMode &&
!isOfflineMode;
final health = healthMap[source];
final healthStatus = _resolveHealthStatus(source, health);
final speedText = _resolveSpeedText(source, health);
return _SourceChip(
source: source,
isActive: isActive,
ext: ext,
showSpeed: showSpeed,
speedText: speedText,
healthStatus: healthStatus,
onTap: () => onSourceChanged(source),
);
},
),
);
}
_HealthStatus _resolveHealthStatus(
WallpaperSource source,
SourceHealth? health,
) {
if (health == null) return _HealthStatus.normal;
if (!health.isAvailable) return _HealthStatus.unavailable;
if (health.avgMs > 2000) return _HealthStatus.slow;
return _HealthStatus.normal;
}
String _resolveSpeedText(WallpaperSource source, SourceHealth? health) {
if (health != null && health.isAvailable) {
return health.avgMs < 1000
? '${health.avgMs}ms'
: '${(health.avgMs / 1000).toStringAsFixed(1)}s';
}
return source.avgMs < 1000
? '${source.avgMs}ms'
: '${(source.avgMs / 1000).toStringAsFixed(1)}s';
}
}
enum _HealthStatus { normal, slow, unavailable }
class _SourceChip extends StatelessWidget {
const _SourceChip({
this.source,
this.label,
required this.isActive,
required this.ext,
required this.showSpeed,
required this.speedText,
required this.healthStatus,
required this.onTap,
});
final WallpaperSource? source;
final String? label;
final bool isActive;
final AppThemeExtension ext;
final bool showSpeed;
final String speedText;
final _HealthStatus healthStatus;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final isUnavailable = healthStatus == _HealthStatus.unavailable;
final isSlow = healthStatus == _HealthStatus.slow;
final chipColor = isUnavailable
? CupertinoColors.systemGrey
: isActive
? ext.accent
: ext.textSecondary;
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isActive
? (isUnavailable
? CupertinoColors.systemGrey.withValues(alpha: 0.15)
: ext.accent.withValues(alpha: 0.15))
: ext.bgCard.withValues(alpha: 0.5),
borderRadius: AppRadius.pillBorder,
border: Border.all(
color: isActive
? (isUnavailable
? CupertinoColors.systemGrey.withValues(alpha: 0.4)
: ext.accent.withValues(alpha: 0.4))
: ext.bgCard.withValues(alpha: 0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (source != null)
_buildSourceIcon(source!, chipColor, 14),
if (label != null)
Text(
label!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isActive ? ext.accent : ext.textSecondary,
),
),
if (source != null) ...[
const SizedBox(width: 4),
Text(
source!.label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: chipColor,
),
),
],
if (isUnavailable) ...[
const SizedBox(width: 3),
Container(
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(3),
),
child: const Text(
'不可用',
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w600,
color: CupertinoColors.systemGrey,
),
),
),
] else if (isSlow) ...[
const SizedBox(width: 3),
Container(
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: CupertinoColors.systemOrange.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(3),
),
child: const Text(
'',
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w600,
color: CupertinoColors.systemOrange,
),
),
),
] else if (showSpeed && speedText.isNotEmpty) ...[
const SizedBox(width: 3),
Text(
speedText,
style: TextStyle(
fontSize: 9,
color: isActive
? ext.accent.withValues(alpha: 0.6)
: ext.textHint,
),
),
],
],
),
),
);
}
Widget _buildSourceIcon(WallpaperSource source, Color color, double size) {
final iconData = EditorIconData.cupertino(source.iconName);
if (iconData != null) {
return Icon(iconData, size: size, color: color);
}
final svgPath = EditorIconData.svg(source.iconName);
if (svgPath != null) {
return SvgPicture.asset(
svgPath,
width: size,
height: size,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
);
}
return Text(source.emoji, style: TextStyle(fontSize: size));
}
}