Compare commits
No commits in common. "173872364a49bc7656482f005b7011327bc54525" and "79d4e32e3e5cab4d72393f32b503f977baa15c98" have entirely different histories.
173872364a
...
79d4e32e3e
@ -13,7 +13,6 @@ import '../config/facebook_config.dart';
|
|||||||
abstract final class AdjustEvents {
|
abstract final class AdjustEvents {
|
||||||
static final _fb = FacebookAppEvents();
|
static final _fb = FacebookAppEvents();
|
||||||
static final _fbLog = Logger(
|
static final _fbLog = Logger(
|
||||||
filter: FacebookConfig.debugLogs ? ProductionFilter() : DevelopmentFilter(),
|
|
||||||
printer: PrettyPrinter(methodCount: 0, lineLength: 120),
|
printer: PrettyPrinter(methodCount: 0, lineLength: 120),
|
||||||
level: FacebookConfig.debugLogs ? Level.trace : Level.off,
|
level: FacebookConfig.debugLogs ? Level.trace : Level.off,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 = false;
|
static const bool debugLogs = true;
|
||||||
|
|
||||||
/// AES 密钥
|
/// AES 密钥
|
||||||
static const String aesKey = 'liyP4LkMfP68XvCt';
|
static const String aesKey = 'liyP4LkMfP68XvCt';
|
||||||
|
|||||||
@ -178,16 +178,10 @@ 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/第三方支付开关可能未更新(其它字段仍已写入)',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,28 +307,9 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) {
|
static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) {
|
||||||
if (!commonRes.isSuccess) {
|
if (!commonRes.isSuccess || commonRes.data == null) return false;
|
||||||
_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) {
|
if (commonData == null) return false;
|
||||||
_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();
|
||||||
@ -381,44 +356,23 @@ 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, st) {
|
} catch (e, _) {
|
||||||
_logMsg('referrer/common_info 后台任务异常: $e');
|
_logMsg('referrer/common_info 后台任务失败: $e');
|
||||||
_logMsg('referrer/common_info 堆栈: $st');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,7 @@ import 'package:logger/logger.dart';
|
|||||||
import '../api/api_config.dart';
|
import '../api/api_config.dart';
|
||||||
|
|
||||||
/// 统一应用日志,提升可读性:时间戳、级别、标签、格式化输出。
|
/// 统一应用日志,提升可读性:时间戳、级别、标签、格式化输出。
|
||||||
///
|
/// release 下默认仅输出 warning/error;ApiConfig.debugLogs=true 时放开全部级别。
|
||||||
/// [Logger] 默认 [DevelopmentFilter] 依赖 `assert`,在 release/profile 下会屏蔽**全部**日志。
|
|
||||||
/// 正式包排障时设 [ApiConfig.debugLogs]=true,此处改用 [ProductionFilter] 才可见 debug/info。
|
|
||||||
/// release 且 debugLogs=false 时仍不输出(warning 也会被 filter 掉,与原先行为一致)。
|
|
||||||
///
|
///
|
||||||
/// 使用示例:
|
/// 使用示例:
|
||||||
/// final _log = AppLogger('GenerateVideo');
|
/// final _log = AppLogger('GenerateVideo');
|
||||||
@ -22,9 +19,6 @@ class AppLogger {
|
|||||||
|
|
||||||
static Logger get _instance {
|
static Logger get _instance {
|
||||||
_logger ??= Logger(
|
_logger ??= Logger(
|
||||||
filter: (!kDebugMode && ApiConfig.debugLogs)
|
|
||||||
? ProductionFilter()
|
|
||||||
: DevelopmentFilter(),
|
|
||||||
printer: PrettyPrinter(
|
printer: PrettyPrinter(
|
||||||
methodCount: 0,
|
methodCount: 0,
|
||||||
errorMethodCount: 6,
|
errorMethodCount: 6,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
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,14 +38,12 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
|
|
||||||
/// 略低于首页:网格边缘格子可见但比例偏低时也应计入「在屏」
|
/// 略低于首页:网格边缘格子可见但比例偏低时也应计入「在屏」
|
||||||
static const double _videoVisibilityThreshold = 0.08;
|
static const double _videoVisibilityThreshold = 0.08;
|
||||||
/// 可见格子按曝光比例排序,同时最多自动播放 [_maxConcurrentGalleryVideos] 路;滑出后 [VideoCard] 释放解码器
|
/// 一屏约 2×(3~4) 个格子;不再强行只播 2 路(会总挑中间行)。滑出后 [VideoCard] 会释放解码器,此处仅防极端滚动时实例过多
|
||||||
static const int _maxConcurrentGalleryVideos = 4;
|
static const int _maxConcurrentGalleryVideos = 16;
|
||||||
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)或首帧未稳定时执行,导致预览不自动播放。
|
||||||
@ -149,41 +147,6 @@ 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(() {
|
||||||
@ -221,7 +184,6 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
_visibleVideoIndices = {};
|
_visibleVideoIndices = {};
|
||||||
_userPausedVideoIndices.clear();
|
_userPausedVideoIndices.clear();
|
||||||
_cardVisibleFraction.clear();
|
_cardVisibleFraction.clear();
|
||||||
_localCoverPaths = {};
|
|
||||||
} else {
|
} else {
|
||||||
_tasks = [..._tasks, ...list];
|
_tasks = [..._tasks, ...list];
|
||||||
}
|
}
|
||||||
@ -231,7 +193,6 @@ 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();
|
||||||
});
|
});
|
||||||
@ -353,7 +314,6 @@ 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;
|
||||||
@ -376,8 +336,14 @@ class _GalleryScreenState extends State<GalleryScreen> {
|
|||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: VideoCard(
|
child: VideoCard(
|
||||||
key: ValueKey(detectorKey),
|
key: ValueKey(detectorKey),
|
||||||
imageUrl: coverSpecs.imageUrl,
|
imageUrl: media.imageUrl ??
|
||||||
cover: coverSpecs.cover,
|
videoUrl ??
|
||||||
|
'',
|
||||||
|
cover: hasVideo
|
||||||
|
? _VideoThumbnailCover(
|
||||||
|
videoUrl: media.videoUrl!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
videoUrl: hasVideo ? videoUrl : null,
|
videoUrl: hasVideo ? videoUrl : null,
|
||||||
credits: '50',
|
credits: '50',
|
||||||
showCreditsBadge: false,
|
showCreditsBadge: false,
|
||||||
@ -408,3 +374,61 @@ 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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
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,13 +3,10 @@ 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 =>
|
||||||
@ -33,16 +30,13 @@ 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, taskId: itemTaskId));
|
items.add(GalleryMediaItem(imageUrl: first));
|
||||||
} 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) {
|
||||||
@ -53,15 +47,9 @@ class GalleryTaskItem {
|
|||||||
? reconnect.toInt()
|
? reconnect.toInt()
|
||||||
: 1;
|
: 1;
|
||||||
if (imgType == 2) {
|
if (imgType == 2) {
|
||||||
items.add(GalleryMediaItem(
|
items.add(GalleryMediaItem(videoUrl: reconfigure));
|
||||||
videoUrl: reconfigure,
|
|
||||||
taskId: itemTaskId,
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
items.add(GalleryMediaItem(
|
items.add(GalleryMediaItem(imageUrl: reconfigure));
|
||||||
imageUrl: reconfigure,
|
|
||||||
taskId: itemTaskId,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,7 +76,7 @@ class GalleryTaskItem {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
return GalleryTaskItem(
|
return GalleryTaskItem(
|
||||||
taskId: treeId,
|
taskId: (json['tree'] as num?)?.toInt() ?? 0,
|
||||||
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,23 +37,9 @@ 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(
|
GalleryMediaItem? _mediaItemFromProgressData(Map<String, dynamic> data) {
|
||||||
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;
|
||||||
@ -67,9 +53,9 @@ GalleryMediaItem? _mediaItemFromProgressData(
|
|||||||
? reconnect.toInt()
|
? reconnect.toInt()
|
||||||
: 1;
|
: 1;
|
||||||
if (imgType == 2) {
|
if (imgType == 2) {
|
||||||
return GalleryMediaItem(videoUrl: reconfigure, taskId: taskId);
|
return GalleryMediaItem(videoUrl: reconfigure);
|
||||||
}
|
}
|
||||||
return GalleryMediaItem(imageUrl: reconfigure, taskId: taskId);
|
return GalleryMediaItem(imageUrl: reconfigure);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate Video Progress screen - matches Pencil rSN3T (video), Eghqc (label)
|
/// Generate Video Progress screen - matches Pencil rSN3T (video), Eghqc (label)
|
||||||
@ -167,10 +153,7 @@ class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final mediaItem = _mediaItemFromProgressData(
|
final mediaItem = _mediaItemFromProgressData(data);
|
||||||
data,
|
|
||||||
taskId: _parseProgressTaskId(widget.taskId),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pushReplacementNamed(
|
Navigator.of(context).pushReplacementNamed(
|
||||||
'/result',
|
'/result',
|
||||||
arguments: mediaItem,
|
arguments: mediaItem,
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import 'dart:async' show unawaited;
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
@ -18,7 +17,6 @@ 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';
|
||||||
@ -261,14 +259,6 @@ 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;
|
||||||
/// 可见格子按曝光比例排序,同时最多 [_maxConcurrentHomeVideos] 路;滑出后 [VideoCard] 释放解码器
|
/// 凡达到阈值的格子均可播;滑出后 [VideoCard] 会释放解码器,此处仅防极端情况
|
||||||
static const int _maxConcurrentHomeVideos = 4;
|
static const int _maxConcurrentHomeVideos = 16;
|
||||||
|
|
||||||
void _onGridCardVisibilityChanged(int index, VisibilityInfo info) {
|
void _onGridCardVisibilityChanged(int index, VisibilityInfo info) {
|
||||||
if (!mounted || !widget.isActive) return;
|
if (!mounted || !widget.isActive) return;
|
||||||
|
|||||||
@ -328,18 +328,16 @@ class _VideoCardState extends State<VideoCard> {
|
|||||||
onTap: widget.onGenerateSimilar,
|
onTap: widget.onGenerateSimilar,
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: widget.cover ??
|
child: widget.cover ??
|
||||||
(widget.imageUrl.isEmpty
|
CachedNetworkImage(
|
||||||
? Container(color: AppColors.surfaceAlt)
|
imageUrl: widget.imageUrl,
|
||||||
: CachedNetworkImage(
|
fit: BoxFit.cover,
|
||||||
imageUrl: widget.imageUrl,
|
placeholder: (_, __) => Container(
|
||||||
fit: BoxFit.cover,
|
color: AppColors.surfaceAlt,
|
||||||
placeholder: (_, __) => Container(
|
),
|
||||||
color: AppColors.surfaceAlt,
|
errorWidget: (_, __, ___) => Container(
|
||||||
),
|
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.15+26
|
version: 1.1.14+25
|
||||||
|
|
||||||
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