打包:1.1.15版本打包

This commit is contained in:
ivan 2026-03-31 09:42:49 +08:00
parent 26caaa46ac
commit 173872364a
10 changed files with 244 additions and 100 deletions

View File

@ -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';

View File

@ -178,10 +178,16 @@ class AuthService {
items: items,
);
_applyScreenSecure(safeArea);
} else {
_logMsg('common_info: surge 解析为 nullJSON 顶层非对象?');
}
} 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');
}
}
}

View File

@ -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×(34) 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(),
);
},
);
}
}

View 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;
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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();

View File

@ -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;

View File

@ -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)

View File

@ -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'