优化:更新我的相册项目保存过期提示

This commit is contained in:
ivan 2026-04-02 16:47:36 +08:00
parent 173872364a
commit aa54b15406
5 changed files with 978 additions and 100 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8", "version": "2.9",
"children": [ "children": [
{ {
"type": "frame", "type": "frame",
@ -1585,6 +1585,97 @@
20 20
], ],
"children": [ "children": [
{
"type": "frame",
"id": "vmPs1",
"name": "Retention notice",
"width": "fill_container",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 135,
"size": {
"height": 1
},
"colors": [
{
"color": "#FFFBEB",
"position": 0
},
{
"color": "#FEF3C7",
"position": 1
}
]
},
"cornerRadius": 14,
"stroke": {
"align": "inside",
"thickness": 1,
"fill": "#FBBF24"
},
"effect": {
"type": "shadow",
"shadowType": "outer",
"color": "#F59E0B22",
"offset": {
"x": 0,
"y": 2
},
"blur": 8
},
"layout": "vertical",
"gap": 6,
"padding": [
14,
16
],
"children": [
{
"type": "frame",
"id": "NI5YU",
"name": "row",
"width": "fill_container",
"gap": 12,
"children": [
{
"type": "icon_font",
"id": "FDa5s",
"name": "icon",
"width": 18,
"height": 18,
"iconFontName": "timer",
"iconFontFamily": "lucide",
"fill": "#D97706"
},
{
"type": "frame",
"id": "pEPL4",
"name": "col",
"width": "fill_container",
"layout": "vertical",
"gap": 6,
"children": [
{
"type": "text",
"id": "zU1J8",
"name": "hintText",
"fill": "#78350F",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "Content is valid for 24 hours. Please s ave it before it expires",
"lineHeight": 1.4,
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "600"
}
]
}
]
}
]
},
{ {
"type": "frame", "type": "frame",
"id": "g1ePM", "id": "g1ePM",
@ -1631,7 +1722,72 @@
}, },
"blur": 12 "blur": 12
}, },
"layout": "none" "layout": "none",
"children": [
{
"type": "frame",
"id": "lThZm",
"x": 0,
"y": 176,
"name": "Card meta",
"width": 165,
"height": 72,
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 180,
"size": {
"height": 1
},
"colors": [
{
"color": "#00000000",
"position": 0
},
{
"color": "#000000D9",
"position": 1
}
]
},
"layout": "vertical",
"gap": 2,
"padding": [
2,
12,
6,
12
],
"justifyContent": "end",
"children": [
{
"type": "text",
"id": "3RTGn",
"fill": "#FFFFFF",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "Created 2026-03-30 09:12",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "500"
},
{
"type": "text",
"id": "osDVy",
"fill": "#E5E7EB",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "23h 48m left",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 9,
"fontWeight": "normal"
}
]
}
]
}, },
{ {
"type": "frame", "type": "frame",
@ -1663,7 +1819,72 @@
}, },
"blur": 12 "blur": 12
}, },
"layout": "none" "layout": "none",
"children": [
{
"type": "frame",
"id": "fUbKb",
"x": 0,
"y": 176,
"name": "Card meta",
"width": 165,
"height": 72,
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 180,
"size": {
"height": 1
},
"colors": [
{
"color": "#00000000",
"position": 0
},
{
"color": "#000000D9",
"position": 1
}
]
},
"layout": "vertical",
"gap": 2,
"padding": [
2,
12,
6,
12
],
"justifyContent": "end",
"children": [
{
"type": "text",
"id": "co3Aw",
"fill": "#FFFFFF",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "Created 2026-03-29 18:05",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "500"
},
{
"type": "text",
"id": "Gyp3m",
"fill": "#FDE68A",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "7h 12m left",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 9,
"fontWeight": "normal"
}
]
}
]
} }
] ]
}, },
@ -1705,7 +1926,72 @@
}, },
"blur": 12 "blur": 12
}, },
"layout": "none" "layout": "none",
"children": [
{
"type": "frame",
"id": "UOw9c",
"x": 0,
"y": 176,
"name": "Card meta",
"width": 165,
"height": 72,
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 180,
"size": {
"height": 1
},
"colors": [
{
"color": "#00000000",
"position": 0
},
{
"color": "#000000D9",
"position": 1
}
]
},
"layout": "vertical",
"gap": 2,
"padding": [
2,
12,
6,
12
],
"justifyContent": "end",
"children": [
{
"type": "text",
"id": "8hHai",
"fill": "#FFFFFF",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "Created 2026-03-29 02:40",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "500"
},
{
"type": "text",
"id": "wg34o",
"fill": "#FCD34D",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "2h 5m left",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 9,
"fontWeight": "normal"
}
]
}
]
}, },
{ {
"type": "frame", "type": "frame",
@ -1737,7 +2023,72 @@
}, },
"blur": 12 "blur": 12
}, },
"layout": "none" "layout": "none",
"children": [
{
"type": "frame",
"id": "7D4XR",
"x": 0,
"y": 176,
"name": "Card meta",
"width": 165,
"height": 72,
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 180,
"size": {
"height": 1
},
"colors": [
{
"color": "#00000000",
"position": 0
},
{
"color": "#000000D9",
"position": 1
}
]
},
"layout": "vertical",
"gap": 2,
"padding": [
2,
12,
6,
12
],
"justifyContent": "end",
"children": [
{
"type": "text",
"id": "39n7h",
"fill": "#FFFFFF",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "Created 2026-03-28 21:08",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "500"
},
{
"type": "text",
"id": "kH3RX",
"fill": "#FCA5A5",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "32m left",
"lineHeight": 1.25,
"fontFamily": "Inter",
"fontSize": 9,
"fontWeight": "normal"
}
]
}
]
} }
] ]
} }
@ -1946,9 +2297,11 @@
{ {
"type": "frame", "type": "frame",
"id": "ZE4Rm", "id": "ZE4Rm",
"x": 0,
"y": 0,
"name": "topNav", "name": "topNav",
"enabled": false, "enabled": false,
"width": "fill_container", "width": "fill_container(0)",
"height": 56, "height": 56,
"fill": "#FFFFFF", "fill": "#FFFFFF",
"effect": { "effect": {
@ -4220,6 +4573,8 @@
{ {
"type": "icon_font", "type": "icon_font",
"id": "a3uQs", "id": "a3uQs",
"x": 0,
"y": 0,
"enabled": false, "enabled": false,
"width": 72, "width": 72,
"height": 72, "height": 72,
@ -4564,6 +4919,44 @@
"fontSize": 14, "fontSize": 14,
"fontWeight": "normal" "fontWeight": "normal"
}, },
{
"type": "text",
"id": "mIiKO",
"name": "verifyLabel",
"fill": "#71717A",
"content": "Type DELETE to confirm",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "500"
},
{
"type": "frame",
"id": "2XdIX",
"name": "verifyInput",
"width": "fill_container",
"height": 48,
"fill": "#FAFAFA",
"cornerRadius": 12,
"stroke": {
"align": "inside",
"thickness": 1,
"fill": "#E4E4E7"
},
"layout": "vertical",
"padding": 16,
"children": [
{
"type": "text",
"id": "GrZIb",
"name": "placeholderText",
"fill": "#A1A1AA",
"content": "Type DELETE here",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
},
{ {
"type": "frame", "type": "frame",
"id": "0HXLh", "id": "0HXLh",

View File

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math' show max;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
import '../../core/api/api_config.dart'; import '../../core/api/api_config.dart';
@ -15,6 +17,94 @@ import '../home/widgets/video_card.dart';
import 'gallery_upload_cover_store.dart'; import 'gallery_upload_cover_store.dart';
import 'models/gallery_task_item.dart'; import 'models/gallery_task_item.dart';
DateTime? _galleryDateTimeFromRaw(int raw) {
if (raw <= 0) return null;
if (raw > 1000000000000) {
return DateTime.fromMillisecondsSinceEpoch(raw);
}
return DateTime.fromMillisecondsSinceEpoch(raw * 1000);
}
String? _galleryCardCreatedLine(GalleryMediaItem m) {
final uncover = m.createTimeText?.trim();
if (uncover != null && uncover.isNotEmpty) {
return 'Created $uncover';
}
final dt = _galleryDateTimeFromRaw(m.createTime);
if (dt == null) return null;
String two(int n) => n.toString().padLeft(2, '0');
return 'Created ${dt.year}-${two(dt.month)}-${two(dt.day)} '
'${two(dt.hour)}:${two(dt.minute)}';
}
String? _galleryCardRemainingLine(GalleryMediaItem m) {
final dt = _galleryDateTimeFromRaw(m.createTime);
if (dt == null) return null;
final expires = dt.add(const Duration(hours: 24));
final left = expires.difference(DateTime.now());
if (left.isNegative) return 'Expired';
return '${left.inHours}h ${left.inMinutes.remainder(60)}m left';
}
Color _galleryCardRemainingColor(GalleryMediaItem m) {
final dt = _galleryDateTimeFromRaw(m.createTime);
if (dt == null) return const Color(0xFFE5E7EB);
final expires = dt.add(const Duration(hours: 24));
final left = expires.difference(DateTime.now());
if (left.isNegative) return AppColors.textMuted;
if (left.inHours < 8) return const Color(0xFFFDE68A);
return const Color(0xFFE5E7EB);
}
/// PencilRetention noticehpwBg / vmPs1
class _GalleryRetentionBanner extends StatelessWidget {
const _GalleryRetentionBanner();
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFFFBEB), Color(0xFFFEF3C7)],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFFBBF24)),
boxShadow: const [
BoxShadow(
color: Color(0x22F59E0B),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(LucideIcons.timer, size: 18, color: Color(0xFFD97706)),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Content is valid for 24 hours. Please save it before '
'it expires.',
style: TextStyle(
color: Color(0xFF78350F),
fontSize: 12,
fontWeight: FontWeight.w600,
height: 1.4,
fontFamily: 'Inter',
),
),
),
],
),
);
}
}
/// Gallery screen - matches Pencil hpwBg /// Gallery screen - matches Pencil hpwBg
class GalleryScreen extends StatefulWidget { class GalleryScreen extends StatefulWidget {
const GalleryScreen({super.key, required this.isActive}); const GalleryScreen({super.key, required this.isActive});
@ -44,6 +134,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
final Set<int> _userPausedVideoIndices = {}; final Set<int> _userPausedVideoIndices = {};
final Map<int, double> _cardVisibleFraction = {}; final Map<int, double> _cardVisibleFraction = {};
Timer? _visibilityDebounce; Timer? _visibilityDebounce;
Timer? _remainingLabelTicker;
/// taskId(tree) -> 使 /// taskId(tree) -> 使
Map<int, String> _localCoverPaths = {}; Map<int, String> _localCoverPaths = {};
@ -74,6 +165,10 @@ class _GalleryScreenState extends State<GalleryScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_scrollController.addListener(_onScroll); _scrollController.addListener(_onScroll);
_remainingLabelTicker =
Timer.periodic(const Duration(minutes: 1), (_) {
if (mounted) setState(() {});
});
if (widget.isActive) _loadTasks(refresh: true); if (widget.isActive) _loadTasks(refresh: true);
} }
@ -90,6 +185,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
@override @override
void dispose() { void dispose() {
_remainingLabelTicker?.cancel();
_visibilityDebounce?.cancel(); _visibilityDebounce?.cancel();
_scrollController.dispose(); _scrollController.dispose();
super.dispose(); super.dispose();
@ -184,6 +280,49 @@ class _GalleryScreenState extends State<GalleryScreen> {
setState(() => _localCoverPaths = paths); setState(() => _localCoverPaths = paths);
} }
void _showGalleryMessage(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
/// pending / finished + URL SnackBar
void _onGalleryMediaTap(GalleryMediaItem media) {
final raw = media.listingRaw;
final disp = media.listingDisplay;
if (galleryListingIsInProgress(raw, disp)) {
final tid = media.taskId;
if (tid == null || tid <= 0) {
_showGalleryMessage('Cannot open progress: missing task id.');
return;
}
final path = _localCoverPaths[tid];
final pathOk = path != null &&
path.isNotEmpty &&
File(path).existsSync();
Navigator.of(context).pushNamed(
'/progress',
arguments: {
'taskId': tid,
if (pathOk) 'imagePath': path,
},
);
return;
}
if (galleryListingIsFinishedSuccess(raw, disp)) {
if (!galleryMediaHasRemoteUrl(media)) {
_showGalleryMessage(
'Media is not ready yet. Please wait or pull to refresh.',
);
return;
}
Navigator.of(context).pushNamed('/result', arguments: media);
return;
}
_showGalleryMessage(galleryListingBlockedHint(raw, disp));
}
Future<void> _loadTasks({bool refresh = true}) async { Future<void> _loadTasks({bool refresh = true}) async {
if (refresh) { if (refresh) {
setState(() { setState(() {
@ -270,7 +409,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
appBar: const PreferredSize( appBar: const PreferredSize(
preferredSize: Size.fromHeight(56), preferredSize: Size.fromHeight(56),
child: TopNavBar( child: TopNavBar(
title: 'My Gallery', title: 'Gallery',
), ),
), ),
body: _loading body: _loading
@ -301,104 +440,157 @@ class _GalleryScreenState extends State<GalleryScreen> {
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => _loadTasks(refresh: true), onRefresh: () => _loadTasks(refresh: true),
child: _gridItems.isEmpty && !_loading child: _gridItems.isEmpty && !_loading
? SingleChildScrollView( ? CustomScrollView(
physics:
const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: constraints.maxHeight - 100,
child: const Center(
child: Text(
'No images yet',
style: TextStyle(
color: AppColors.textSecondary,
),
),
),
),
)
: GridView.builder(
physics: physics:
const AlwaysScrollableScrollPhysics(), const AlwaysScrollableScrollPhysics(),
controller: _scrollController, controller: _scrollController,
cacheExtent: 800, cacheExtent: 800,
padding: EdgeInsets.fromLTRB( slivers: [
AppSpacing.screenPadding, SliverToBoxAdapter(
AppSpacing.xl, child: Padding(
AppSpacing.screenPadding, padding: const EdgeInsets.fromLTRB(
AppSpacing.screenPaddingLarge + AppSpacing.screenPadding,
(_loadingMore ? 48.0 : 0), AppSpacing.xl,
), AppSpacing.screenPadding,
gridDelegate: 16,
const SliverGridDelegateWithFixedCrossAxisCount( ),
crossAxisCount: 2, child: const _GalleryRetentionBanner(),
childAspectRatio: 165 / 248, ),
mainAxisSpacing: AppSpacing.xl, ),
crossAxisSpacing: AppSpacing.xl, SliverFillRemaining(
), hasScrollBody: false,
itemCount: _gridItems.length + child: SizedBox(
(_loadingMore ? 1 : 0), height: max(0.0, constraints.maxHeight - 220),
itemBuilder: (context, index) { child: const Center(
if (index >= _gridItems.length) { child: Text(
return const Center( 'No images yet',
child: Padding( style: TextStyle(
padding: EdgeInsets.all(16), color: AppColors.textSecondary,
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
), ),
), ),
), ),
); ),
} ),
final media = _gridItems[index]; ],
final coverSpecs = _galleryCardCover(media); )
final videoUrl = media.videoUrl; : CustomScrollView(
final hasVideo = physics:
videoUrl != null && videoUrl.isNotEmpty; const AlwaysScrollableScrollPhysics(),
final detectorKey = controller: _scrollController,
'gallery_${index}_${videoUrl ?? media.imageUrl ?? ''}'; cacheExtent: 800,
void openResult() { slivers: [
Navigator.of(context).pushNamed( SliverToBoxAdapter(
'/result', child: Padding(
arguments: media, padding: const EdgeInsets.fromLTRB(
); AppSpacing.screenPadding,
} AppSpacing.xl,
AppSpacing.screenPadding,
16,
),
child: const _GalleryRetentionBanner(),
),
),
SliverPadding(
padding: EdgeInsets.fromLTRB(
AppSpacing.screenPadding,
0,
AppSpacing.screenPadding,
AppSpacing.screenPaddingLarge +
(_loadingMore ? 48.0 : 0),
),
sliver: SliverGrid(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 165 / 248,
mainAxisSpacing: AppSpacing.xl,
crossAxisSpacing: AppSpacing.xl,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index >= _gridItems.length) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 24,
height: 24,
child:
CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
final media = _gridItems[index];
final coverSpecs =
_galleryCardCover(media);
final videoUrl = media.videoUrl;
final hasVideo = videoUrl != null &&
videoUrl.isNotEmpty;
final detectorKey =
'gallery_${index}_${videoUrl ?? media.imageUrl ?? ''}';
final created =
_galleryCardCreatedLine(media);
final remaining =
_galleryCardRemainingLine(
media);
return VisibilityDetector( return VisibilityDetector(
key: ValueKey(detectorKey), key: ValueKey(detectorKey),
onVisibilityChanged: hasVideo onVisibilityChanged: hasVideo
? (info) => ? (info) =>
_onGridCardVisibilityChanged( _onGridCardVisibilityChanged(
index, info) index, info)
: (_) {}, : (_) {},
child: RepaintBoundary( child: RepaintBoundary(
child: VideoCard( child: VideoCard(
key: ValueKey(detectorKey), key: ValueKey(detectorKey),
imageUrl: coverSpecs.imageUrl, imageUrl: coverSpecs.imageUrl,
cover: coverSpecs.cover, cover: coverSpecs.cover,
videoUrl: hasVideo ? videoUrl : null, videoUrl: hasVideo
credits: '50', ? videoUrl
showCreditsBadge: false, : null,
showBottomGenerateButton: false, credits: '50',
isActive: widget.isActive && showCreditsBadge: false,
hasVideo && showBottomGenerateButton:
_visibleVideoIndices false,
.contains(index) && cardMetaCreatedText:
!_userPausedVideoIndices created,
.contains(index), cardMetaRemainingText:
onPlayRequested: () => setState(() => remaining,
_userPausedVideoIndices cardMetaRemainingColor:
.remove(index)), _galleryCardRemainingColor(
onStopRequested: () => setState(() => media),
_userPausedVideoIndices topRightStatusText:
.add(index)), media.listingDisplay,
onGenerateSimilar: openResult, isActive: widget.isActive &&
hasVideo &&
_visibleVideoIndices
.contains(index) &&
!_userPausedVideoIndices
.contains(index),
onPlayRequested: () =>
setState(() =>
_userPausedVideoIndices
.remove(index)),
onStopRequested: () =>
setState(() =>
_userPausedVideoIndices
.add(index)),
onGenerateSimilar: () =>
_onGalleryMediaTap(media),
),
),
);
},
childCount: _gridItems.length +
(_loadingMore ? 1 : 0),
), ),
), ),
); ),
}, ],
), ),
), ),
), ),

View File

@ -1,15 +1,175 @@
/// listing 1 | 2 |
String _galleryListingLabelEnglish(int listing) {
switch (listing) {
case 1:
return 'Queued';
case 2:
return 'Processing';
case 3:
return 'Completed';
case 4:
return 'Timed out';
case 5:
return 'Error';
case 6:
return 'Aborted';
case 0:
return 'Pending';
default:
return 'Unknown';
}
}
/// `listing` finished int
String listingDisplayFromApi(dynamic raw) {
if (raw == null) return '';
if (raw is int) return _galleryListingLabelEnglish(raw);
if (raw is num) return _galleryListingLabelEnglish(raw.toInt());
final s = raw.toString().trim();
if (s.isEmpty) return '';
final asInt = int.tryParse(s);
if (asInt != null && s == asInt.toString()) {
return _galleryListingLabelEnglish(asInt);
}
return s;
}
bool galleryMediaHasRemoteUrl(GalleryMediaItem m) {
bool http(String? x) {
if (x == null) return false;
final t = x.trim();
return t.startsWith('http://') || t.startsWith('https://');
}
return http(m.imageUrl) || http(m.videoUrl);
}
/// pending / queued / processing 0·1·2
bool galleryListingIsInProgress(dynamic raw, String display) {
final d = display.trim().toLowerCase();
if (d == 'pending' ||
d == 'queued' ||
d == 'processing' ||
d == 'in progress' ||
d == 'running') {
return true;
}
if (raw != null) {
if (raw is int && (raw == 0 || raw == 1 || raw == 2)) return true;
if (raw is num) {
final v = raw.toInt();
if (v == 0 || v == 1 || v == 2) return true;
}
final s = raw.toString().trim().toLowerCase();
if (s == 'pending' ||
s == 'queued' ||
s == 'processing' ||
s == 'in_progress' ||
s == 'in progress' ||
s == 'running') {
return true;
}
final n = int.tryParse(s);
if (n != null && (n == 0 || n == 1 || n == 2)) return true;
}
return false;
}
/// finished / completed 3
bool galleryListingIsFinishedSuccess(dynamic raw, String display) {
final d = display.trim().toLowerCase();
if (d == 'finished' ||
d == 'completed' ||
d == 'complete' ||
d == 'success' ||
d == 'done') {
return true;
}
if (raw != null) {
if (raw is int && raw == 3) return true;
if (raw is num && raw.toInt() == 3) return true;
final s = raw.toString().trim().toLowerCase();
if (s == 'finished' ||
s == 'completed' ||
s == 'complete' ||
s == 'success' ||
s == 'done') {
return true;
}
if (int.tryParse(s) == 3) return true;
}
return false;
}
/// /
String galleryListingBlockedHint(dynamic raw, String display) {
int? code;
if (raw is int) {
code = raw;
} else if (raw is num) {
code = raw.toInt();
} else if (raw != null) {
final s = raw.toString().trim().toLowerCase();
if (s == 'timeout' || s == 'timed out' || s == 'timed_out') {
code = 4;
} else if (s == 'error' || s == 'failed' || s == 'failure') {
code = 5;
} else if (s == 'aborted' || s == 'cancelled' || s == 'canceled') {
code = 6;
} else {
code = int.tryParse(s);
}
}
switch (code) {
case 4:
return 'This task has timed out.';
case 5:
return 'This task failed. Please try again.';
case 6:
return 'This task was cancelled.';
default:
final low = display.trim().toLowerCase();
if (low.contains('timeout') || low.contains('timed out')) {
return 'This task has timed out.';
}
if (low.contains('error') || low.contains('fail')) {
return 'This task failed. Please try again.';
}
if (low.contains('abort') || low.contains('cancel')) {
return 'This task was cancelled.';
}
return 'This item is not available yet.';
}
}
/// reconfigure=imgUrl reconnect(imgType) 0=1= /// reconfigure=imgUrl reconnect(imgType) 0=1=
class GalleryMediaItem { class GalleryMediaItem {
const GalleryMediaItem({ GalleryMediaItem({
this.imageUrl, this.imageUrl,
this.videoUrl, this.videoUrl,
this.taskId, this.taskId,
}) : assert(imageUrl != null || videoUrl != null); this.createTime = 0,
this.createTimeText,
this.listingDisplay = '',
this.listingRaw,
}) : assert(
(imageUrl != null && imageUrl.isNotEmpty) ||
(videoUrl != null && videoUrl.isNotEmpty) ||
(taskId != null && taskId > 0),
);
final String? imageUrl; final String? imageUrl;
final String? videoUrl; final String? videoUrl;
/// `tree` /// `tree`
final int? taskId; final int? taskId;
/// discover
final int createTime;
/// uncover
final String? createTimeText;
/// listing
final String listingDisplay;
/// `listing`/ [listingDisplay]
final dynamic listingRaw;
/// reconnect==0 1 /// reconnect==0 1
bool get isVideo => bool get isVideo =>
@ -36,13 +196,24 @@ class GalleryTaskItem {
final treeRaw = json['tree'] as num?; final treeRaw = json['tree'] as num?;
final treeId = treeRaw?.toInt() ?? 0; final treeId = treeRaw?.toInt() ?? 0;
final itemTaskId = treeId > 0 ? treeId : null; final itemTaskId = treeId > 0 ? treeId : null;
final createTime = (json['discover'] as num?)?.toInt() ?? 0;
final createTimeText = json['uncover'] as String?;
final listingRaw = json['listing'];
final listingDisplay = listingDisplayFromApi(listingRaw);
final downsample = json['downsample'] as List<dynamic>? ?? []; final downsample = json['downsample'] as List<dynamic>? ?? [];
final items = <GalleryMediaItem>[]; final items = <GalleryMediaItem>[];
// downsample的array[0] // downsample的array[0]
if (downsample.isNotEmpty) { if (downsample.isNotEmpty) {
final first = downsample[0]; final first = downsample[0];
if (first is String) { if (first is String && first.trim().isNotEmpty) {
items.add(GalleryMediaItem(imageUrl: first, taskId: itemTaskId)); items.add(GalleryMediaItem(
imageUrl: first,
taskId: itemTaskId,
createTime: createTime,
createTimeText: createTimeText,
listingDisplay: listingDisplay,
listingRaw: listingRaw,
));
} else if (first is Map<String, dynamic>) { } else if (first is Map<String, dynamic>) {
final reconfigure = first['reconfigure'] as String?; final reconfigure = first['reconfigure'] as String?;
if (reconfigure != null && reconfigure.isNotEmpty) { if (reconfigure != null && reconfigure.isNotEmpty) {
@ -56,17 +227,35 @@ class GalleryTaskItem {
items.add(GalleryMediaItem( items.add(GalleryMediaItem(
videoUrl: reconfigure, videoUrl: reconfigure,
taskId: itemTaskId, taskId: itemTaskId,
createTime: createTime,
createTimeText: createTimeText,
listingDisplay: listingDisplay,
listingRaw: listingRaw,
)); ));
} else { } else {
items.add(GalleryMediaItem( items.add(GalleryMediaItem(
imageUrl: reconfigure, imageUrl: reconfigure,
taskId: itemTaskId, taskId: itemTaskId,
createTime: createTime,
createTimeText: createTimeText,
listingDisplay: listingDisplay,
listingRaw: listingRaw,
)); ));
} }
} }
} }
// //
} }
// downsample URL tree
if (items.isEmpty && itemTaskId != null && itemTaskId > 0) {
items.add(GalleryMediaItem(
taskId: itemTaskId,
createTime: createTime,
createTimeText: createTimeText,
listingDisplay: listingDisplay,
listingRaw: listingRaw,
));
}
// for (final item in downsample) { // for (final item in downsample) {
// if (item is String) { // if (item is String) {
// items.add(GalleryMediaItem(imageUrl: item)); // items.add(GalleryMediaItem(imageUrl: item));

View File

@ -28,6 +28,10 @@ class VideoCard extends StatefulWidget {
required this.onPlayRequested, required this.onPlayRequested,
required this.onStopRequested, required this.onStopRequested,
this.onGenerateSimilar, this.onGenerateSimilar,
this.cardMetaCreatedText,
this.cardMetaRemainingText,
this.cardMetaRemainingColor,
this.topRightStatusText,
}); });
final String imageUrl; final String imageUrl;
@ -42,6 +46,13 @@ class VideoCard extends StatefulWidget {
final VoidCallback onPlayRequested; final VoidCallback onPlayRequested;
final VoidCallback onStopRequested; final VoidCallback onStopRequested;
final VoidCallback? onGenerateSimilar; final VoidCallback? onGenerateSimilar;
/// Gallery Created
final String? cardMetaCreatedText;
/// Gallery null
final String? cardMetaRemainingText;
final Color? cardMetaRemainingColor;
/// Gallery
final String? topRightStatusText;
@override @override
State<VideoCard> createState() => _VideoCardState(); State<VideoCard> createState() => _VideoCardState();
@ -369,6 +380,66 @@ class _VideoCardState extends State<VideoCard> {
), ),
), ),
), ),
if (widget.cardMetaCreatedText != null &&
widget.cardMetaCreatedText!.isNotEmpty)
Positioned(
left: 0,
right: 0,
bottom: 0,
height: 72,
child: IgnorePointer(
child: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Color(0xD9000000),
],
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 2, 12, 6),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
widget.cardMetaCreatedText!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.surface,
fontSize: 10,
fontWeight: FontWeight.w500,
height: 1.25,
fontFamily: 'Inter',
),
),
if (widget.cardMetaRemainingText != null &&
widget.cardMetaRemainingText!.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
widget.cardMetaRemainingText!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: widget.cardMetaRemainingColor ??
const Color(0xFFE5E7EB),
fontSize: 9,
fontWeight: FontWeight.normal,
height: 1.25,
fontFamily: 'Inter',
),
),
],
],
),
),
),
),
),
if (widget.showCreditsBadge) if (widget.showCreditsBadge)
Positioned( Positioned(
top: 12, top: 12,
@ -406,6 +477,39 @@ class _VideoCardState extends State<VideoCard> {
), ),
), ),
), ),
if (widget.topRightStatusText != null &&
widget.topRightStatusText!.trim().isNotEmpty)
Positioned(
top: 12,
right: 12,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.58,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.overlayDark,
borderRadius: BorderRadius.circular(14),
),
child: Text(
widget.topRightStatusText!.trim(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColors.surface,
fontSize: 10,
fontWeight: FontWeight.w600,
fontFamily: 'Inter',
),
),
),
),
),
if (widget.showBottomGenerateButton) if (widget.showBottomGenerateButton)
Positioned( Positioned(
bottom: 16, bottom: 16,

View File

@ -1,7 +1,7 @@
name: pets_hero_ai name: pets_hero_ai
description: PetsHero AI Application. description: PetsHero AI Application.
publish_to: 'none' publish_to: 'none'
version: 1.1.15+26 version: 1.1.16+27
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'