Files
xianyan/lib/features/template/wallpaper_gallery/wallpaper_masonry_grid.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

425 lines
12 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-12
/// 作用: 瀑布流布局展示壁纸 + URL三级回退 + 已加载优先排序
/// + CachedNetworkImage + shimmer骨架屏 + 选择态
/// 上次更新: 从shared/widgets迁移至features/template模块简化template导入路径调整shared/widgets路径
/// ============================================================
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:shimmer/shimmer.dart';
import '../../../core/services/data/image_cache_manager.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/utils/logger.dart';
import '../../../shared/widgets/media/safe_cached_image.dart';
import '../models/template_models.dart';
import '../services/wallpaper_favorite_service.dart';
class WallpaperMasonryGrid extends StatelessWidget {
const WallpaperMasonryGrid({
super.key,
required this.items,
required this.isLoading,
required this.hasMore,
required this.selectedId,
required this.onSelect,
required this.onLoadMore,
required this.onItemTap,
this.onImageLoaded,
this.crossAxisCount = 2,
});
final List<WallpaperItem> items;
final bool isLoading;
final bool hasMore;
final String? selectedId;
final ValueChanged<String> onSelect;
final VoidCallback onLoadMore;
final ValueChanged<WallpaperItem> onItemTap;
final ValueChanged<String>? onImageLoaded;
final int crossAxisCount;
/// 三级回退获取最佳图片URL: thumbnailUrl → previewUrl → imageUrl
static String resolveImageUrl(WallpaperItem item) {
if (item.thumbnailUrl.isNotEmpty) return item.thumbnailUrl;
if (item.previewUrl.isNotEmpty) return item.previewUrl;
return item.imageUrl;
}
/// 将已加载(有缓存)的项排到前面
static List<WallpaperItem> sortLoadedFirst(
List<WallpaperItem> items,
Set<String> cachedIds,
) {
final cached = <WallpaperItem>[];
final uncached = <WallpaperItem>[];
for (final item in items) {
if (cachedIds.contains(item.id)) {
cached.add(item);
} else {
uncached.add(item);
}
}
return [...cached, ...uncached];
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
if (isLoading && items.isEmpty) {
return Center(
child: CupertinoActivityIndicator(radius: 14, color: ext.textSecondary),
);
}
if (items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.photo, size: 48, color: ext.textHint),
const SizedBox(height: 12),
Text(
'暂无壁纸',
style: TextStyle(color: ext.textSecondary, fontSize: 16),
),
],
),
);
}
return MasonryGridView.count(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
itemCount: items.length + (hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= items.length) {
WidgetsBinding.instance.addPostFrameCallback((_) => onLoadMore());
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CupertinoActivityIndicator(radius: 12)),
);
}
final item = items[index];
final isSelected = selectedId == item.id;
return _WallpaperCard(
item: item,
isSelected: isSelected,
ext: ext,
onSelect: () => onSelect(item.id),
onTap: () => onItemTap(item),
onImageLoaded: onImageLoaded,
);
},
);
}
}
class _WallpaperCard extends StatefulWidget {
const _WallpaperCard({
required this.item,
required this.isSelected,
required this.ext,
required this.onSelect,
required this.onTap,
this.onImageLoaded,
});
final WallpaperItem item;
final bool isSelected;
final AppThemeExtension ext;
final VoidCallback onSelect;
final VoidCallback onTap;
final ValueChanged<String>? onImageLoaded;
@override
State<_WallpaperCard> createState() => _WallpaperCardState();
}
class _WallpaperCardState extends State<_WallpaperCard> {
bool _isCached = false;
bool _checkedCache = false;
bool _imageLoadedNotified = false;
late bool _isFavorite;
@override
void initState() {
super.initState();
_isFavorite = WallpaperFavoriteService.isFavorite(widget.item.id);
_checkCache();
}
@override
void didUpdateWidget(covariant _WallpaperCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.item.id != widget.item.id) {
_isFavorite = WallpaperFavoriteService.isFavorite(widget.item.id);
}
}
// 修复9: 缓存检查添加超时保护防止文件I/O阻塞
Future<void> _checkCache() async {
final url = WallpaperMasonryGrid.resolveImageUrl(widget.item);
if (url.isEmpty) return;
try {
final file = await CustomCacheManager.instance
.getFileFromCache(url)
.timeout(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isCached = file != null;
_checkedCache = true;
});
if (file != null) {
_notifyImageLoaded(url);
}
}
} catch (e) {
Log.w('壁纸缓存检查失败: $e');
if (mounted) setState(() => _checkedCache = true);
}
}
void _notifyImageLoaded(String url) {
if (_imageLoadedNotified || url.isEmpty) return;
_imageLoadedNotified = true;
widget.onImageLoaded?.call(url);
}
@override
Widget build(BuildContext context) {
final ext = widget.ext;
final item = widget.item;
final imageUrl = WallpaperMasonryGrid.resolveImageUrl(item);
return GestureDetector(
onTap: () {
widget.onSelect();
widget.onTap();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: AppRadius.mdBorder,
border: widget.isSelected
? Border.all(color: ext.accent, width: 2.5)
: Border.all(
color: ext.bgCard.withValues(alpha: 0.3),
width: 0.5,
),
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
_buildImage(imageUrl, ext),
if (_isCached && _checkedCache)
Positioned(top: 6, left: 6, child: _buildCachedBadge(ext)),
Positioned(top: 6, right: 6, child: _buildFavoriteButton(ext)),
if (widget.isSelected)
Positioned(top: 6, right: 6, child: _buildSelectCheck(ext)),
Positioned.fill(child: _buildGradientOverlay()),
Positioned(left: 0, right: 0, bottom: 0, child: _buildInfo(ext)),
],
),
),
);
}
Widget _buildImage(String imageUrl, AppThemeExtension ext) {
if (imageUrl.isEmpty) {
return Container(
height: 160,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ext.accent.withValues(alpha: 0.3), ext.bgCard],
),
),
child: Center(
child: Icon(CupertinoIcons.photo, color: ext.textHint, size: 32),
),
);
}
final aspectRatio = widget.item.width > 0 && widget.item.height > 0
? widget.item.width / widget.item.height
: 16 / 9;
final height = aspectRatio > 0 ? 200.0 / aspectRatio : 200.0;
final clampedHeight = height.clamp(120.0, 320.0);
return SizedBox(
height: clampedHeight,
child: SafeCachedImage(
url: imageUrl,
fit: BoxFit.cover,
httpHeaders: const {'User-Agent': 'Xianyan/1.0'},
fadeInDuration: const Duration(milliseconds: 300),
onLoaded: () => _notifyImageLoaded(imageUrl),
placeholder: (_, __) => Shimmer.fromColors(
baseColor: ext.bgCard,
highlightColor: ext.bgElevated,
child: Container(
height: clampedHeight,
decoration: BoxDecoration(
color: ext.bgCard,
borderRadius: AppRadius.mdBorder,
),
),
),
errorWidget: (_, __, ___) => Container(
height: clampedHeight,
color: ext.bgCard,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.photo, color: ext.textHint, size: 24),
const SizedBox(height: 4),
Text(
'加载失败',
style: TextStyle(fontSize: 10, color: ext.textHint),
),
],
),
),
),
),
);
}
Widget _buildCachedBadge(AppThemeExtension ext) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFF34C759).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'已缓存',
style: TextStyle(
color: CupertinoColors.white,
fontSize: 8,
fontWeight: FontWeight.w600,
),
),
);
}
/// 收藏心形按钮
Widget _buildFavoriteButton(AppThemeExtension ext) {
return GestureDetector(
onTap: () async {
await WallpaperFavoriteService.toggle(widget.item);
if (mounted) {
setState(() {
_isFavorite = !_isFavorite;
});
}
},
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.35),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Icon(
_isFavorite ? CupertinoIcons.heart_fill : CupertinoIcons.heart,
size: 14,
color: _isFavorite ? const Color(0xFFFF3B30) : CupertinoColors.white,
),
),
);
}
Widget _buildSelectCheck(AppThemeExtension ext) {
return Container(
width: 22,
height: 22,
decoration: BoxDecoration(color: ext.accent, shape: BoxShape.circle),
child: const Icon(
CupertinoIcons.checkmark,
size: 12,
color: CupertinoColors.white,
),
);
}
Widget _buildGradientOverlay() {
return const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Color(0x80000000)],
),
),
);
}
Widget _buildInfo(AppThemeExtension ext) {
final item = widget.item;
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (item.title.isNotEmpty)
Text(
item.title.length > 12
? '${item.title.substring(0, 12)}'
: item.title,
style: const TextStyle(
color: Color(0xCCFFFFFF),
fontSize: 10,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Row(
children: [
if (item.resolution.isNotEmpty) _buildBadge(item.resolution),
const SizedBox(width: 4),
if (item.views > 0) _buildBadge(_formatCount(item.views)),
],
),
],
),
);
}
Widget _buildBadge(String text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(3),
),
child: Text(
text,
style: const TextStyle(color: Color(0xB3FFFFFF), fontSize: 8),
),
);
}
String _formatCount(int count) {
if (count >= 10000) return '${(count / 10000).toStringAsFixed(1)}w';
if (count >= 1000) return '${(count / 1000).toStringAsFixed(1)}k';
return '$count';
}
}