打包:1.1.15版本打包
This commit is contained in:
parent
26caaa46ac
commit
173872364a
@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
/// petsHeroAI API 配置
|
/// petsHeroAI API 配置
|
||||||
abstract final class ApiConfig {
|
abstract final class ApiConfig {
|
||||||
/// 调试日志:true 时 release 包也输出 debug/info(正式包调试用,上线前改 false)
|
/// 调试日志:true 时 release 包也输出 debug/info(正式包调试用,上线前改 false)
|
||||||
static const bool debugLogs = true;
|
static const bool debugLogs = false;
|
||||||
|
|
||||||
/// AES 密钥
|
/// AES 密钥
|
||||||
static const String aesKey = 'liyP4LkMfP68XvCt';
|
static const String aesKey = 'liyP4LkMfP68XvCt';
|
||||||
|
|||||||
@ -178,10 +178,16 @@ class AuthService {
|
|||||||
items: items,
|
items: items,
|
||||||
);
|
);
|
||||||
_applyScreenSecure(safeArea);
|
_applyScreenSecure(safeArea);
|
||||||
|
} else {
|
||||||
|
_logMsg('common_info: surge 解析为 null(JSON 顶层非对象?)');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logMsg('surge JSON 解析失败: $e');
|
_logMsg('surge JSON 解析失败: $e');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_logMsg(
|
||||||
|
'common_info: 无 surge 或 surge 为空串,extConfig/第三方支付开关可能未更新(其它字段仍已写入)',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,9 +313,28 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) {
|
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>?;
|
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();
|
final before = _HomeExtSnapshot.capture();
|
||||||
_saveCommonInfoToState(commonData);
|
_saveCommonInfoToState(commonData);
|
||||||
final after = _HomeExtSnapshot.capture();
|
final after = _HomeExtSnapshot.capture();
|
||||||
@ -356,23 +381,44 @@ class AuthService {
|
|||||||
_logMsg('referrer(gg) 失败: code=${rGg.code} msg=${rGg.msg}');
|
_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(
|
final commonRes = await UserApi.getCommonInfo(
|
||||||
sentinel: ApiConfig.appId,
|
sentinel: ApiConfig.appId,
|
||||||
asset: uid,
|
asset: uid,
|
||||||
);
|
);
|
||||||
if (_applyCommonInfoAndDidHomeStructureChange(commonRes)) {
|
if (_applyCommonInfoAndDidHomeStructureChange(commonRes)) {
|
||||||
UserState.requestHomeFullReload();
|
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 {
|
static Future<void> _runPostLoginReferrerWork(String uid, String deviceId) async {
|
||||||
try {
|
try {
|
||||||
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
|
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
|
||||||
} catch (e, _) {
|
} catch (e, st) {
|
||||||
_logMsg('referrer/common_info 后台任务失败: $e');
|
_logMsg('referrer/common_info 后台任务异常: $e');
|
||||||
|
_logMsg('referrer/common_info 堆栈: $st');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.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 '../../shared/widgets/top_nav_bar.dart';
|
||||||
import '../home/widgets/video_card.dart';
|
import '../home/widgets/video_card.dart';
|
||||||
|
|
||||||
|
import 'gallery_upload_cover_store.dart';
|
||||||
import 'models/gallery_task_item.dart';
|
import 'models/gallery_task_item.dart';
|
||||||
import 'video_thumbnail_cache.dart';
|
|
||||||
|
|
||||||
/// Gallery screen - matches Pencil hpwBg
|
/// Gallery screen - matches Pencil hpwBg
|
||||||
class GalleryScreen extends StatefulWidget {
|
class GalleryScreen extends StatefulWidget {
|
||||||
@ -38,12 +38,14 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
|
|
||||||
/// 略低于首页:网格边缘格子可见但比例偏低时也应计入「在屏」
|
/// 略低于首页:网格边缘格子可见但比例偏低时也应计入「在屏」
|
||||||
static const double _videoVisibilityThreshold = 0.08;
|
static const double _videoVisibilityThreshold = 0.08;
|
||||||
/// 一屏约 2×(3~4) 个格子;不再强行只播 2 路(会总挑中间行)。滑出后 [VideoCard] 会释放解码器,此处仅防极端滚动时实例过多
|
/// 可见格子按曝光比例排序,同时最多自动播放 [_maxConcurrentGalleryVideos] 路;滑出后 [VideoCard] 释放解码器
|
||||||
static const int _maxConcurrentGalleryVideos = 16;
|
static const int _maxConcurrentGalleryVideos = 4;
|
||||||
Set<int> _visibleVideoIndices = {};
|
Set<int> _visibleVideoIndices = {};
|
||||||
final Set<int> _userPausedVideoIndices = {};
|
final Set<int> _userPausedVideoIndices = {};
|
||||||
final Map<int, double> _cardVisibleFraction = {};
|
final Map<int, double> _cardVisibleFraction = {};
|
||||||
Timer? _visibilityDebounce;
|
Timer? _visibilityDebounce;
|
||||||
|
/// taskId(tree) -> 本地上传封面路径(接口无封面时使用)
|
||||||
|
Map<int, String> _localCoverPaths = {};
|
||||||
|
|
||||||
/// [IndexedStack] 非当前 Tab 时子树可见性常为 0;首次切到本页时若仅打一次 [notifyNow],
|
/// [IndexedStack] 非当前 Tab 时子树可见性常为 0;首次切到本页时若仅打一次 [notifyNow],
|
||||||
/// 可能在 Grid 尚未挂载(仍在 loading)或首帧未稳定时执行,导致预览不自动播放。
|
/// 可能在 Grid 尚未挂载(仍在 loading)或首帧未稳定时执行,导致预览不自动播放。
|
||||||
@ -147,6 +149,41 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
return true;
|
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 {
|
Future<void> _loadTasks({bool refresh = true}) async {
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -184,6 +221,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
_visibleVideoIndices = {};
|
_visibleVideoIndices = {};
|
||||||
_userPausedVideoIndices.clear();
|
_userPausedVideoIndices.clear();
|
||||||
_cardVisibleFraction.clear();
|
_cardVisibleFraction.clear();
|
||||||
|
_localCoverPaths = {};
|
||||||
} else {
|
} else {
|
||||||
_tasks = [..._tasks, ...list];
|
_tasks = [..._tasks, ...list];
|
||||||
}
|
}
|
||||||
@ -193,6 +231,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
_loadingMore = false;
|
_loadingMore = false;
|
||||||
_hasLoadedOnce = true;
|
_hasLoadedOnce = true;
|
||||||
});
|
});
|
||||||
|
await _refreshLocalCoverPaths();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) _scheduleGalleryVisibilityRefresh();
|
if (mounted) _scheduleGalleryVisibilityRefresh();
|
||||||
});
|
});
|
||||||
@ -314,6 +353,7 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final media = _gridItems[index];
|
final media = _gridItems[index];
|
||||||
|
final coverSpecs = _galleryCardCover(media);
|
||||||
final videoUrl = media.videoUrl;
|
final videoUrl = media.videoUrl;
|
||||||
final hasVideo =
|
final hasVideo =
|
||||||
videoUrl != null && videoUrl.isNotEmpty;
|
videoUrl != null && videoUrl.isNotEmpty;
|
||||||
@ -336,14 +376,8 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: VideoCard(
|
child: VideoCard(
|
||||||
key: ValueKey(detectorKey),
|
key: ValueKey(detectorKey),
|
||||||
imageUrl: media.imageUrl ??
|
imageUrl: coverSpecs.imageUrl,
|
||||||
videoUrl ??
|
cover: coverSpecs.cover,
|
||||||
'',
|
|
||||||
cover: hasVideo
|
|
||||||
? _VideoThumbnailCover(
|
|
||||||
videoUrl: media.videoUrl!,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
videoUrl: hasVideo ? videoUrl : null,
|
videoUrl: hasVideo ? videoUrl : null,
|
||||||
credits: '50',
|
credits: '50',
|
||||||
showCreditsBadge: false,
|
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({
|
const GalleryMediaItem({
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
this.videoUrl,
|
this.videoUrl,
|
||||||
|
this.taskId,
|
||||||
}) : assert(imageUrl != null || videoUrl != null);
|
}) : assert(imageUrl != null || videoUrl != null);
|
||||||
|
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
final String? videoUrl; // 视频地址,用于生成封面
|
final String? videoUrl;
|
||||||
|
/// 与列表项 `tree` 一致,用于匹配本地上传封面缓存
|
||||||
|
final int? taskId;
|
||||||
|
|
||||||
/// reconnect==0 为视频,1 或其他为图片
|
/// reconnect==0 为视频,1 或其他为图片
|
||||||
bool get isVideo =>
|
bool get isVideo =>
|
||||||
@ -30,13 +33,16 @@ class GalleryTaskItem {
|
|||||||
final List<GalleryMediaItem> mediaItems;
|
final List<GalleryMediaItem> mediaItems;
|
||||||
|
|
||||||
factory GalleryTaskItem.fromJson(Map<String, dynamic> json) {
|
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 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) {
|
||||||
items.add(GalleryMediaItem(imageUrl: first));
|
items.add(GalleryMediaItem(imageUrl: first, taskId: itemTaskId));
|
||||||
} 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) {
|
||||||
@ -47,9 +53,15 @@ class GalleryTaskItem {
|
|||||||
? reconnect.toInt()
|
? reconnect.toInt()
|
||||||
: 1;
|
: 1;
|
||||||
if (imgType == 2) {
|
if (imgType == 2) {
|
||||||
items.add(GalleryMediaItem(videoUrl: reconfigure));
|
items.add(GalleryMediaItem(
|
||||||
|
videoUrl: reconfigure,
|
||||||
|
taskId: itemTaskId,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
items.add(GalleryMediaItem(imageUrl: reconfigure));
|
items.add(GalleryMediaItem(
|
||||||
|
imageUrl: reconfigure,
|
||||||
|
taskId: itemTaskId,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,7 +88,7 @@ class GalleryTaskItem {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
return GalleryTaskItem(
|
return GalleryTaskItem(
|
||||||
taskId: (json['tree'] as num?)?.toInt() ?? 0,
|
taskId: treeId,
|
||||||
state: json['listing']?.toString() ?? '',
|
state: json['listing']?.toString() ?? '',
|
||||||
taskType: (json['cipher'] as num?)?.toInt() ?? 0,
|
taskType: (json['cipher'] as num?)?.toInt() ?? 0,
|
||||||
createTime: (json['discover'] 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
|
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).
|
/// Build GalleryMediaItem from /v1/image/progress response (data = sidekick).
|
||||||
/// curate[].reconfigure = imgUrl, reconnect(imgType): 2=视频,1或其他=图片
|
/// 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>?;
|
final curate = data['curate'] as List<dynamic>?;
|
||||||
if (curate == null || curate.isEmpty) return null;
|
if (curate == null || curate.isEmpty) return null;
|
||||||
final first = curate.first;
|
final first = curate.first;
|
||||||
@ -53,9 +67,9 @@ GalleryMediaItem? _mediaItemFromProgressData(Map<String, dynamic> data) {
|
|||||||
? reconnect.toInt()
|
? reconnect.toInt()
|
||||||
: 1;
|
: 1;
|
||||||
if (imgType == 2) {
|
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)
|
/// Generate Video Progress screen - matches Pencil rSN3T (video), Eghqc (label)
|
||||||
@ -153,7 +167,10 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final mediaItem = _mediaItemFromProgressData(data);
|
final mediaItem = _mediaItemFromProgressData(
|
||||||
|
data,
|
||||||
|
taskId: _parseProgressTaskId(widget.taskId),
|
||||||
|
);
|
||||||
Navigator.of(context).pushReplacementNamed(
|
Navigator.of(context).pushReplacementNamed(
|
||||||
'/result',
|
'/result',
|
||||||
arguments: mediaItem,
|
arguments: mediaItem,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async' show unawaited;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ import '../../core/theme/app_spacing.dart';
|
|||||||
import '../../core/theme/app_typography.dart';
|
import '../../core/theme/app_typography.dart';
|
||||||
import '../../core/user/account_refresh.dart';
|
import '../../core/user/account_refresh.dart';
|
||||||
import '../../core/user/user_state.dart';
|
import '../../core/user/user_state.dart';
|
||||||
|
import '../../features/gallery/gallery_upload_cover_store.dart';
|
||||||
import '../../features/gallery/video_thumbnail_cache.dart';
|
import '../../features/gallery/video_thumbnail_cache.dart';
|
||||||
import '../../features/home/home_playback_resume.dart';
|
import '../../features/home/home_playback_resume.dart';
|
||||||
import '../../features/home/models/task_item.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 taskData = createRes.data as Map<String, dynamic>?;
|
||||||
final taskId = taskData?['tree'];
|
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();
|
await refreshAccount();
|
||||||
|
|||||||
@ -150,8 +150,8 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
/// 与 Gallery 一致:边缘格子可见比例偏低时也应参与自动播
|
/// 与 Gallery 一致:边缘格子可见比例偏低时也应参与自动播
|
||||||
static const double _videoVisibilityThreshold = 0.08;
|
static const double _videoVisibilityThreshold = 0.08;
|
||||||
/// 凡达到阈值的格子均可播;滑出后 [VideoCard] 会释放解码器,此处仅防极端情况
|
/// 可见格子按曝光比例排序,同时最多 [_maxConcurrentHomeVideos] 路;滑出后 [VideoCard] 释放解码器
|
||||||
static const int _maxConcurrentHomeVideos = 16;
|
static const int _maxConcurrentHomeVideos = 4;
|
||||||
|
|
||||||
void _onGridCardVisibilityChanged(int index, VisibilityInfo info) {
|
void _onGridCardVisibilityChanged(int index, VisibilityInfo info) {
|
||||||
if (!mounted || !widget.isActive) return;
|
if (!mounted || !widget.isActive) return;
|
||||||
|
|||||||
@ -328,16 +328,18 @@ class _VideoCardState extends State<VideoCard> {
|
|||||||
onTap: widget.onGenerateSimilar,
|
onTap: widget.onGenerateSimilar,
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: widget.cover ??
|
child: widget.cover ??
|
||||||
CachedNetworkImage(
|
(widget.imageUrl.isEmpty
|
||||||
imageUrl: widget.imageUrl,
|
? Container(color: AppColors.surfaceAlt)
|
||||||
fit: BoxFit.cover,
|
: CachedNetworkImage(
|
||||||
placeholder: (_, __) => Container(
|
imageUrl: widget.imageUrl,
|
||||||
color: AppColors.surfaceAlt,
|
fit: BoxFit.cover,
|
||||||
),
|
placeholder: (_, __) => Container(
|
||||||
errorWidget: (_, __, ___) => Container(
|
color: AppColors.surfaceAlt,
|
||||||
color: AppColors.surfaceAlt,
|
),
|
||||||
),
|
errorWidget: (_, __, ___) => Container(
|
||||||
),
|
color: AppColors.surfaceAlt,
|
||||||
|
),
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showVideoLayer)
|
if (showVideoLayer)
|
||||||
|
|||||||
@ -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.14+25
|
version: 1.1.15+26
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user