import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import '../../../design/pencil_theme.dart'; final _historyCardDateFormat = DateFormat('MMM d, yyyy'); /// WBRp4 单张卡片:171×182 比例,圆角 20,Download pill。 /// /// [MyTaskItem.resultUrl] 为生成结果地址(逻辑字段 `imgUrl`,线网常为 `boost`),可能是图片或视频。 class HistoryGridCard extends StatelessWidget { const HistoryGridCard({ super.key, required this.item, this.localCoverPath, this.onTap, this.onDownload, }); final MyTaskItem item; final String? localCoverPath; final VoidCallback? onTap; final VoidCallback? onDownload; @override Widget build(BuildContext context) { final url = item.resultUrl?.trim() ?? ''; final dateLabel = _formatCardDate(item.createTime); final remainder = _remainderLabel(item.createTime); return LayoutBuilder( builder: (context, c) { final w = c.maxWidth; final h = w * (182 / 171); return SizedBox( width: w, height: h, child: GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Stack( clipBehavior: Clip.none, children: [ Positioned( left: 0, top: 0, width: w, height: h, child: ClipRRect( borderRadius: BorderRadius.circular(20), child: _HistoryThumb( networkUrl: url.isNotEmpty ? url : null, localPath: localCoverPath, ), ), ), Positioned( left: 0, right: 0, top: 0, height: 64, child: ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(20), ), child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.55), Colors.black.withValues(alpha: 0), ], ), ), ), ), ), Positioned( left: 10, top: 10, right: 10, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( dateLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: GoogleFonts.inter( fontSize: 9, fontWeight: FontWeight.w500, color: Colors.white.withValues(alpha: 0.95), shadows: const [ Shadow( blurRadius: 4, color: Color(0x40000000), offset: Offset(0, 1), ), ], ), ), const SizedBox(height: 2), Text( remainder, style: GoogleFonts.inter( fontSize: 9, fontWeight: FontWeight.w600, color: PencilTheme.underlineGold, shadows: const [ Shadow( blurRadius: 4, color: Color(0x40000000), offset: Offset(0, 1), ), ], ), ), ], ), ), Positioned( right: 8, bottom: 8, child: Material( color: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(21), side: const BorderSide(color: PencilTheme.downloadPillBorder), ), child: InkWell( onTap: onDownload, borderRadius: BorderRadius.circular(21), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'Download', style: TextStyle( fontFamily: 'BonheurRoyale', fontSize: 12, color: PencilTheme.downloadPillInk, ), ), const SizedBox(width: 4), Icon(Icons.download_rounded, size: 10, color: PencilTheme.downloadPillInk), ], ), ), ), ), ), ], ), ), ); }, ); } } bool _urlLooksLikeVideo(String url) { if (url.isEmpty) return false; final lower = url.toLowerCase(); const hints = ['.mp4', '.m3u8', '.webm', '.mov', '.mkv', '.avi']; return hints.any((h) => lower.contains(h)); } /// 图片:网络图;视频:仅封面(优先本地上传缩略图),不播放视频。 class _HistoryThumb extends StatelessWidget { const _HistoryThumb({ this.networkUrl, this.localPath, }); final String? networkUrl; final String? localPath; @override Widget build(BuildContext context) { final net = networkUrl?.trim() ?? ''; if (net.isNotEmpty) { final uri = Uri.tryParse(net); if (uri != null && (uri.isScheme('http') || uri.isScheme('https'))) { if (_urlLooksLikeVideo(net)) { if (localPath != null && localPath!.isNotEmpty) { return Image.file(File(localPath!), fit: BoxFit.cover); } return const _VideoCoverPlaceholder(); } return CachedNetworkImage(imageUrl: net, fit: BoxFit.cover); } } if (localPath != null && localPath!.isNotEmpty) { return Image.file(File(localPath!), fit: BoxFit.cover); } return Container(color: PencilTheme.cardThumbBg); } } /// 无本地封面时的视频任务占位(不拉起解码器)。 class _VideoCoverPlaceholder extends StatelessWidget { const _VideoCoverPlaceholder(); @override Widget build(BuildContext context) { return ColoredBox( color: PencilTheme.cardThumbBg, child: Center( child: Icon( Icons.videocam_outlined, size: 40, color: PencilTheme.inkSoft.withValues(alpha: 0.45), ), ), ); } } /// 与 [MyTaskItem.createTime] 一致:毫秒时间戳、秒级时间戳或 ISO 字符串。 DateTime? _parseCreateTime(String? raw) { if (raw == null || raw.isEmpty) return null; final asInt = int.tryParse(raw.trim()); if (asInt != null) { var ms = asInt; if (asInt < 2000000000) ms = asInt * 1000; return DateTime.fromMillisecondsSinceEpoch(ms); } return DateTime.tryParse(raw.trim()); } String _formatCardDate(String? raw) { final dt = _parseCreateTime(raw); if (dt == null) return '—'; return _historyCardDateFormat.format(dt); } String _remainderLabel(String? createTimeRaw) { final created = _parseCreateTime(createTimeRaw); if (created == null) return '—'; final deadline = created.add(const Duration(hours: 24)); final left = deadline.difference(DateTime.now()); if (left.isNegative) return 'Expired'; final h = left.inHours; final m = left.inMinutes.remainder(60); return '${h}h ${m.toString().padLeft(2, '0')}m left'; }