Compare commits

..

2 Commits

Author SHA1 Message Date
ivan
173872364a 打包:1.1.15版本打包 2026-03-31 09:42:49 +08:00
ivan
26caaa46ac 新增:日志开关 2026-03-30 18:18:45 +08:00
12 changed files with 252 additions and 101 deletions

View File

@ -13,6 +13,7 @@ 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,
); );

View File

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

View File

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

View File

@ -4,7 +4,10 @@ import 'package:logger/logger.dart';
import '../api/api_config.dart'; import '../api/api_config.dart';
/// ///
/// release warning/errorApiConfig.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');
@ -19,6 +22,9 @@ 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,

View File

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

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({ 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,

View File

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

View File

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

View File

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

View File

@ -328,7 +328,9 @@ 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
? Container(color: AppColors.surfaceAlt)
: CachedNetworkImage(
imageUrl: widget.imageUrl, imageUrl: widget.imageUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: (_, __) => Container( placeholder: (_, __) => Container(
@ -337,7 +339,7 @@ class _VideoCardState extends State<VideoCard> {
errorWidget: (_, __, ___) => Container( errorWidget: (_, __, ___) => Container(
color: AppColors.surfaceAlt, color: AppColors.surfaceAlt,
), ),
), )),
), ),
), ),
if (showVideoLayer) if (showVideoLayer)

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.14+25 version: 1.1.15+26
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'