打包:1.1.15版本打包
This commit is contained in:
parent
26caaa46ac
commit
173872364a
@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
/// petsHeroAI API 配置
|
||||
abstract final class ApiConfig {
|
||||
/// 调试日志:true 时 release 包也输出 debug/info(正式包调试用,上线前改 false)
|
||||
static const bool debugLogs = true;
|
||||
static const bool debugLogs = false;
|
||||
|
||||
/// AES 密钥
|
||||
static const String aesKey = 'liyP4LkMfP68XvCt';
|
||||
|
||||
@ -178,10 +178,16 @@ class AuthService {
|
||||
items: items,
|
||||
);
|
||||
_applyScreenSecure(safeArea);
|
||||
} else {
|
||||
_logMsg('common_info: surge 解析为 null(JSON 顶层非对象?)');
|
||||
}
|
||||
} catch (e) {
|
||||
_logMsg('surge JSON 解析失败: $e');
|
||||
}
|
||||
} else {
|
||||
_logMsg(
|
||||
'common_info: 无 surge 或 surge 为空串,extConfig/第三方支付开关可能未更新(其它字段仍已写入)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,9 +313,28 @@ class AuthService {
|
||||
}
|
||||
|
||||
static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) {
|
||||
if (!commonRes.isSuccess || commonRes.data == null) return false;
|
||||
if (!commonRes.isSuccess) {
|
||||
_logMsg(
|
||||
'common_info 失败: code=${commonRes.code} msg=${commonRes.msg} '
|
||||
'dataPreview=${_shortDataPreview(commonRes.data)}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (commonRes.data == null) {
|
||||
_logMsg('common_info 失败: code=0 但 data 为 null');
|
||||
return false;
|
||||
}
|
||||
final commonData = commonRes.data as Map<String, dynamic>?;
|
||||
if (commonData == null) return false;
|
||||
if (commonData == null) {
|
||||
_logMsg(
|
||||
'common_info 失败: code=0 但 data 非 Map,类型=${commonRes.data.runtimeType} '
|
||||
'preview=${_shortDataPreview(commonRes.data)}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
_logMsg(
|
||||
'common_info 收到 data,字段 keys=${commonData.keys.toList()}',
|
||||
);
|
||||
final before = _HomeExtSnapshot.capture();
|
||||
_saveCommonInfoToState(commonData);
|
||||
final after = _HomeExtSnapshot.capture();
|
||||
@ -356,23 +381,44 @@ class AuthService {
|
||||
_logMsg('referrer(gg) 失败: code=${rGg.code} msg=${rGg.msg}');
|
||||
}
|
||||
|
||||
_logMsg(
|
||||
'common_info 请求 GET /v1/user/common_info '
|
||||
'sentinel=${ApiConfig.appId} asset=$uid',
|
||||
);
|
||||
final commonRes = await UserApi.getCommonInfo(
|
||||
sentinel: ApiConfig.appId,
|
||||
asset: uid,
|
||||
);
|
||||
if (_applyCommonInfoAndDidHomeStructureChange(commonRes)) {
|
||||
UserState.requestHomeFullReload();
|
||||
} else if (!commonRes.isSuccess) {
|
||||
_logMsg(
|
||||
'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志里预览 [data],避免整段 surge 撑爆日志
|
||||
static String _shortDataPreview(dynamic data, {int maxLen = 240}) {
|
||||
if (data == null) return 'null';
|
||||
if (data is Map) {
|
||||
return 'Map(keys=${data.keys.toList()}, size=${data.length})';
|
||||
}
|
||||
if (data is List) {
|
||||
return 'List(len=${data.length})';
|
||||
}
|
||||
String s;
|
||||
try {
|
||||
s = data is String ? data : jsonEncode(data);
|
||||
} catch (_) {
|
||||
s = data.toString();
|
||||
}
|
||||
if (s.length <= maxLen) return s;
|
||||
return '${s.substring(0, maxLen)}…(len=${s.length})';
|
||||
}
|
||||
|
||||
static Future<void> _runPostLoginReferrerWork(String uid, String deviceId) async {
|
||||
try {
|
||||
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
|
||||
} catch (e, _) {
|
||||
_logMsg('referrer/common_info 后台任务失败: $e');
|
||||
} catch (e, st) {
|
||||
_logMsg('referrer/common_info 后台任务异常: $e');
|
||||
_logMsg('referrer/common_info 堆栈: $st');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
@ -12,8 +12,8 @@ import '../../core/theme/app_spacing.dart';
|
||||
import '../../shared/widgets/top_nav_bar.dart';
|
||||
import '../home/widgets/video_card.dart';
|
||||
|
||||
import 'gallery_upload_cover_store.dart';
|
||||
import 'models/gallery_task_item.dart';
|
||||
import 'video_thumbnail_cache.dart';
|
||||
|
||||
/// Gallery screen - matches Pencil hpwBg
|
||||
class GalleryScreen extends StatefulWidget {
|
||||
@ -38,12 +38,14 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
|
||||
/// 略低于首页:网格边缘格子可见但比例偏低时也应计入「在屏」
|
||||
static const double _videoVisibilityThreshold = 0.08;
|
||||
/// 一屏约 2×(3~4) 个格子;不再强行只播 2 路(会总挑中间行)。滑出后 [VideoCard] 会释放解码器,此处仅防极端滚动时实例过多
|
||||
static const int _maxConcurrentGalleryVideos = 16;
|
||||
/// 可见格子按曝光比例排序,同时最多自动播放 [_maxConcurrentGalleryVideos] 路;滑出后 [VideoCard] 释放解码器
|
||||
static const int _maxConcurrentGalleryVideos = 4;
|
||||
Set<int> _visibleVideoIndices = {};
|
||||
final Set<int> _userPausedVideoIndices = {};
|
||||
final Map<int, double> _cardVisibleFraction = {};
|
||||
Timer? _visibilityDebounce;
|
||||
/// taskId(tree) -> 本地上传封面路径(接口无封面时使用)
|
||||
Map<int, String> _localCoverPaths = {};
|
||||
|
||||
/// [IndexedStack] 非当前 Tab 时子树可见性常为 0;首次切到本页时若仅打一次 [notifyNow],
|
||||
/// 可能在 Grid 尚未挂载(仍在 loading)或首帧未稳定时执行,导致预览不自动播放。
|
||||
@ -147,6 +149,41 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
return true;
|
||||
}
|
||||
|
||||
static const String _defaultGalleryCoverAsset = 'assets/images/logo.png';
|
||||
|
||||
/// 网络封面优先;无则本地上传图(按 tree);再无则默认占位图
|
||||
({String imageUrl, Widget? cover}) _galleryCardCover(GalleryMediaItem media) {
|
||||
final net = media.imageUrl?.trim() ?? '';
|
||||
if (net.isNotEmpty) {
|
||||
return (imageUrl: net, cover: null);
|
||||
}
|
||||
final tid = media.taskId;
|
||||
if (tid != null && tid > 0) {
|
||||
final path = _localCoverPaths[tid];
|
||||
if (path != null && path.isNotEmpty && File(path).existsSync()) {
|
||||
return (
|
||||
imageUrl: '',
|
||||
cover: Image.file(File(path), fit: BoxFit.cover),
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
imageUrl: '',
|
||||
cover: Image.asset(
|
||||
_defaultGalleryCoverAsset,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshLocalCoverPaths() async {
|
||||
if (!mounted) return;
|
||||
final ids = _tasks.map((t) => t.taskId).where((id) => id > 0).toSet();
|
||||
final paths = await GalleryUploadCoverStore.existingPathsForTaskIds(ids);
|
||||
if (!mounted) return;
|
||||
setState(() => _localCoverPaths = paths);
|
||||
}
|
||||
|
||||
Future<void> _loadTasks({bool refresh = true}) async {
|
||||
if (refresh) {
|
||||
setState(() {
|
||||
@ -184,6 +221,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
_visibleVideoIndices = {};
|
||||
_userPausedVideoIndices.clear();
|
||||
_cardVisibleFraction.clear();
|
||||
_localCoverPaths = {};
|
||||
} else {
|
||||
_tasks = [..._tasks, ...list];
|
||||
}
|
||||
@ -193,6 +231,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
_loadingMore = false;
|
||||
_hasLoadedOnce = true;
|
||||
});
|
||||
await _refreshLocalCoverPaths();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _scheduleGalleryVisibilityRefresh();
|
||||
});
|
||||
@ -314,6 +353,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
);
|
||||
}
|
||||
final media = _gridItems[index];
|
||||
final coverSpecs = _galleryCardCover(media);
|
||||
final videoUrl = media.videoUrl;
|
||||
final hasVideo =
|
||||
videoUrl != null && videoUrl.isNotEmpty;
|
||||
@ -336,14 +376,8 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
child: RepaintBoundary(
|
||||
child: VideoCard(
|
||||
key: ValueKey(detectorKey),
|
||||
imageUrl: media.imageUrl ??
|
||||
videoUrl ??
|
||||
'',
|
||||
cover: hasVideo
|
||||
? _VideoThumbnailCover(
|
||||
videoUrl: media.videoUrl!,
|
||||
)
|
||||
: null,
|
||||
imageUrl: coverSpecs.imageUrl,
|
||||
cover: coverSpecs.cover,
|
||||
videoUrl: hasVideo ? videoUrl : null,
|
||||
credits: '50',
|
||||
showCreditsBadge: false,
|
||||
@ -374,61 +408,3 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoThumbnailCover extends StatefulWidget {
|
||||
const _VideoThumbnailCover({required this.videoUrl});
|
||||
|
||||
final String videoUrl;
|
||||
|
||||
@override
|
||||
State<_VideoThumbnailCover> createState() => _VideoThumbnailCoverState();
|
||||
}
|
||||
|
||||
class _VideoThumbnailCoverState extends State<_VideoThumbnailCover> {
|
||||
/// 必须在 State 内固定同一 Future;否则父级每次 build 新建 Future,[FutureBuilder] 会重置并闪一下占位图
|
||||
late Future<Uint8List?> _thumbFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_thumbFuture =
|
||||
VideoThumbnailCache.instance.getThumbnail(widget.videoUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _VideoThumbnailCover oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.videoUrl != widget.videoUrl) {
|
||||
_thumbFuture =
|
||||
VideoThumbnailCache.instance.getThumbnail(widget.videoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Uint8List?>(
|
||||
future: _thumbFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Image.memory(
|
||||
snapshot.data!,
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
child: snapshot.connectionState == ConnectionState.waiting
|
||||
? const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
81
lib/features/gallery/gallery_upload_cover_store.dart
Normal file
81
lib/features/gallery/gallery_upload_cover_store.dart
Normal file
@ -0,0 +1,81 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// 生图流程创建任务后,按接口返回的 `tree`(任务 id)将用户上传的压缩图存一份到本地;
|
||||
/// Gallery 在接口未返回封面 URL 时可用其作为卡片底图。
|
||||
abstract final class GalleryUploadCoverStore {
|
||||
static const String _subdir = 'gallery_upload_covers';
|
||||
static const String _fileExt = '.jpg';
|
||||
|
||||
/// 本地封面最多保留时长,超时文件会在下次读/写前删除。
|
||||
static const Duration maxRetention = Duration(hours: 25);
|
||||
|
||||
static Future<Directory> _directory() async {
|
||||
final base = await getApplicationSupportDirectory();
|
||||
final dir = Directory('${base.path}/$_subdir');
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
static Future<Directory> _directoryAfterPurge() async {
|
||||
final dir = await _directory();
|
||||
await _purgeExpired(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
static Future<void> _purgeExpired(Directory dir) async {
|
||||
if (!await dir.exists()) return;
|
||||
final now = DateTime.now();
|
||||
try {
|
||||
await for (final entity in dir.list(followLinks: false)) {
|
||||
if (entity is! File) continue;
|
||||
final name = entity.uri.pathSegments.last;
|
||||
if (!name.endsWith(_fileExt)) continue;
|
||||
final stat = await entity.stat();
|
||||
if (now.difference(stat.modified) >= maxRetention) {
|
||||
try {
|
||||
await entity.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static File _fileForTask(Directory dir, int taskId) =>
|
||||
File('${dir.path}/$taskId$_fileExt');
|
||||
|
||||
/// [source] 一般为 [compressImageForUpload] 输出的待上传文件。
|
||||
static Future<void> saveForTask(int taskId, File source) async {
|
||||
if (taskId <= 0) return;
|
||||
if (!await source.exists()) return;
|
||||
final dir = await _directoryAfterPurge();
|
||||
final dest = _fileForTask(dir, taskId);
|
||||
await source.copy(dest.path);
|
||||
}
|
||||
|
||||
static Future<String?> pathIfExists(int taskId) async {
|
||||
if (taskId <= 0) return null;
|
||||
final dir = await _directoryAfterPurge();
|
||||
final f = _fileForTask(dir, taskId);
|
||||
return await f.exists() ? f.path : null;
|
||||
}
|
||||
|
||||
/// 仅查询 [ids] 中已有文件的 path,用于列表刷新后一次性填充状态。
|
||||
static Future<Map<int, String>> existingPathsForTaskIds(
|
||||
Iterable<int> ids,
|
||||
) async {
|
||||
final dir = await _directoryAfterPurge();
|
||||
final out = <int, String>{};
|
||||
for (final id in ids) {
|
||||
if (id <= 0) continue;
|
||||
final f = _fileForTask(dir, id);
|
||||
if (await f.exists()) {
|
||||
out[id] = f.path;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@ -3,10 +3,13 @@ class GalleryMediaItem {
|
||||
const GalleryMediaItem({
|
||||
this.imageUrl,
|
||||
this.videoUrl,
|
||||
this.taskId,
|
||||
}) : assert(imageUrl != null || videoUrl != null);
|
||||
|
||||
final String? imageUrl;
|
||||
final String? videoUrl; // 视频地址,用于生成封面
|
||||
final String? videoUrl;
|
||||
/// 与列表项 `tree` 一致,用于匹配本地上传封面缓存
|
||||
final int? taskId;
|
||||
|
||||
/// reconnect==0 为视频,1 或其他为图片
|
||||
bool get isVideo =>
|
||||
@ -30,13 +33,16 @@ class GalleryTaskItem {
|
||||
final List<GalleryMediaItem> mediaItems;
|
||||
|
||||
factory GalleryTaskItem.fromJson(Map<String, dynamic> json) {
|
||||
final treeRaw = json['tree'] as num?;
|
||||
final treeId = treeRaw?.toInt() ?? 0;
|
||||
final itemTaskId = treeId > 0 ? treeId : null;
|
||||
final downsample = json['downsample'] as List<dynamic>? ?? [];
|
||||
final items = <GalleryMediaItem>[];
|
||||
// 只取downsample的array[0]
|
||||
if (downsample.isNotEmpty) {
|
||||
final first = downsample[0];
|
||||
if (first is String) {
|
||||
items.add(GalleryMediaItem(imageUrl: first));
|
||||
items.add(GalleryMediaItem(imageUrl: first, taskId: itemTaskId));
|
||||
} else if (first is Map<String, dynamic>) {
|
||||
final reconfigure = first['reconfigure'] as String?;
|
||||
if (reconfigure != null && reconfigure.isNotEmpty) {
|
||||
@ -47,9 +53,15 @@ class GalleryTaskItem {
|
||||
? reconnect.toInt()
|
||||
: 1;
|
||||
if (imgType == 2) {
|
||||
items.add(GalleryMediaItem(videoUrl: reconfigure));
|
||||
items.add(GalleryMediaItem(
|
||||
videoUrl: reconfigure,
|
||||
taskId: itemTaskId,
|
||||
));
|
||||
} else {
|
||||
items.add(GalleryMediaItem(imageUrl: reconfigure));
|
||||
items.add(GalleryMediaItem(
|
||||
imageUrl: reconfigure,
|
||||
taskId: itemTaskId,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,7 +88,7 @@ class GalleryTaskItem {
|
||||
// }
|
||||
// }
|
||||
return GalleryTaskItem(
|
||||
taskId: (json['tree'] as num?)?.toInt() ?? 0,
|
||||
taskId: treeId,
|
||||
state: json['listing']?.toString() ?? '',
|
||||
taskType: (json['cipher'] as num?)?.toInt() ?? 0,
|
||||
createTime: (json['discover'] as num?)?.toInt() ?? 0,
|
||||
|
||||
@ -37,9 +37,23 @@ double _progressForState(int? state) {
|
||||
return 1.0; // 3, 4, 5, 6
|
||||
}
|
||||
|
||||
int? _parseProgressTaskId(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
if (raw is int) return raw > 0 ? raw : null;
|
||||
if (raw is num) {
|
||||
final v = raw.toInt();
|
||||
return v > 0 ? v : null;
|
||||
}
|
||||
final v = int.tryParse(raw.toString());
|
||||
return (v != null && v > 0) ? v : null;
|
||||
}
|
||||
|
||||
/// Build GalleryMediaItem from /v1/image/progress response (data = sidekick).
|
||||
/// curate[].reconfigure = imgUrl, reconnect(imgType): 2=视频,1或其他=图片
|
||||
GalleryMediaItem? _mediaItemFromProgressData(Map<String, dynamic> data) {
|
||||
GalleryMediaItem? _mediaItemFromProgressData(
|
||||
Map<String, dynamic> data, {
|
||||
int? taskId,
|
||||
}) {
|
||||
final curate = data['curate'] as List<dynamic>?;
|
||||
if (curate == null || curate.isEmpty) return null;
|
||||
final first = curate.first;
|
||||
@ -53,9 +67,9 @@ GalleryMediaItem? _mediaItemFromProgressData(Map<String, dynamic> data) {
|
||||
? reconnect.toInt()
|
||||
: 1;
|
||||
if (imgType == 2) {
|
||||
return GalleryMediaItem(videoUrl: reconfigure);
|
||||
return GalleryMediaItem(videoUrl: reconfigure, taskId: taskId);
|
||||
}
|
||||
return GalleryMediaItem(imageUrl: reconfigure);
|
||||
return GalleryMediaItem(imageUrl: reconfigure, taskId: taskId);
|
||||
}
|
||||
|
||||
/// Generate Video Progress screen - matches Pencil rSN3T (video), Eghqc (label)
|
||||
@ -153,7 +167,10 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!mounted) return;
|
||||
final mediaItem = _mediaItemFromProgressData(data);
|
||||
final mediaItem = _mediaItemFromProgressData(
|
||||
data,
|
||||
taskId: _parseProgressTaskId(widget.taskId),
|
||||
);
|
||||
Navigator.of(context).pushReplacementNamed(
|
||||
'/result',
|
||||
arguments: mediaItem,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'dart:async' show unawaited;
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
@ -17,6 +18,7 @@ import '../../core/theme/app_spacing.dart';
|
||||
import '../../core/theme/app_typography.dart';
|
||||
import '../../core/user/account_refresh.dart';
|
||||
import '../../core/user/user_state.dart';
|
||||
import '../../features/gallery/gallery_upload_cover_store.dart';
|
||||
import '../../features/gallery/video_thumbnail_cache.dart';
|
||||
import '../../features/home/home_playback_resume.dart';
|
||||
import '../../features/home/models/task_item.dart';
|
||||
@ -259,6 +261,14 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
||||
|
||||
final taskData = createRes.data as Map<String, dynamic>?;
|
||||
final taskId = taskData?['tree'];
|
||||
final taskIdInt = taskId is int
|
||||
? taskId
|
||||
: taskId is num
|
||||
? taskId.toInt()
|
||||
: int.tryParse(taskId?.toString() ?? '');
|
||||
if (taskIdInt != null && taskIdInt > 0) {
|
||||
unawaited(GalleryUploadCoverStore.saveForTask(taskIdInt, toUpload));
|
||||
}
|
||||
|
||||
// 创建任务成功后刷新用户账户信息(积分等)
|
||||
await refreshAccount();
|
||||
|
||||
@ -150,8 +150,8 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||
|
||||
/// 与 Gallery 一致:边缘格子可见比例偏低时也应参与自动播
|
||||
static const double _videoVisibilityThreshold = 0.08;
|
||||
/// 凡达到阈值的格子均可播;滑出后 [VideoCard] 会释放解码器,此处仅防极端情况
|
||||
static const int _maxConcurrentHomeVideos = 16;
|
||||
/// 可见格子按曝光比例排序,同时最多 [_maxConcurrentHomeVideos] 路;滑出后 [VideoCard] 释放解码器
|
||||
static const int _maxConcurrentHomeVideos = 4;
|
||||
|
||||
void _onGridCardVisibilityChanged(int index, VisibilityInfo info) {
|
||||
if (!mounted || !widget.isActive) return;
|
||||
|
||||
@ -328,16 +328,18 @@ class _VideoCardState extends State<VideoCard> {
|
||||
onTap: widget.onGenerateSimilar,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: widget.cover ??
|
||||
CachedNetworkImage(
|
||||
imageUrl: widget.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
),
|
||||
),
|
||||
(widget.imageUrl.isEmpty
|
||||
? Container(color: AppColors.surfaceAlt)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: widget.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
if (showVideoLayer)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
name: pets_hero_ai
|
||||
description: PetsHero AI Application.
|
||||
publish_to: 'none'
|
||||
version: 1.1.14+25
|
||||
version: 1.1.15+26
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user