This commit is contained in:
roxy 2026-05-13 13:46:50 +08:00
parent f2e9ac3c60
commit 3103d51d4c
34 changed files with 1032 additions and 435 deletions

View File

@ -288,6 +288,7 @@
"rechargeAgency": "وكالة الشحن", "rechargeAgency": "وكالة الشحن",
"logout": "تسجيل الخروج", "logout": "تسجيل الخروج",
"adminCenter": "مركز الإدارة", "adminCenter": "مركز الإدارة",
"managerCenter": "مركز المدير",
"explore": "استكشاف", "explore": "استكشاف",
"micManagement": "إدارة الميكروفون", "micManagement": "إدارة الميكروفون",
"followList": "قائمة المتابعة", "followList": "قائمة المتابعة",

View File

@ -442,6 +442,7 @@
"bdCenter": "BD সেন্টার", "bdCenter": "BD সেন্টার",
"rechargeAgency": "রিচার্জ এজেন্সি", "rechargeAgency": "রিচার্জ এজেন্সি",
"adminCenter": "অ্যাডমিন সেন্টার", "adminCenter": "অ্যাডমিন সেন্টার",
"managerCenter": "ম্যানেজার সেন্টার",
"goldList": "সোনা তালিকা", "goldList": "সোনা তালিকা",
"followList": "ফলো তালিকা", "followList": "ফলো তালিকা",
"fansList": "ফ্যান তালিকা", "fansList": "ফ্যান তালিকা",

View File

@ -412,6 +412,7 @@
"bdCenter": "BD Center", "bdCenter": "BD Center",
"rechargeAgency": "Recharge Agency", "rechargeAgency": "Recharge Agency",
"adminCenter": "Admin Center", "adminCenter": "Admin Center",
"managerCenter": "Manager Center",
"goldList": "Gold List", "goldList": "Gold List",
"followList": "Follow List", "followList": "Follow List",
"fansList": "Fans List", "fansList": "Fans List",

View File

@ -411,6 +411,7 @@
"bdCenter": "BD Merkezi", "bdCenter": "BD Merkezi",
"rechargeAgency": "Yükleme Temsilciliği", "rechargeAgency": "Yükleme Temsilciliği",
"adminCenter": "Yönetici Merkezi", "adminCenter": "Yönetici Merkezi",
"managerCenter": "Manager Center",
"goldList": "Altın Listesi", "goldList": "Altın Listesi",
"followList": "Takip Listesi", "followList": "Takip Listesi",
"fansList": "Hayran Listesi", "fansList": "Hayran Listesi",

View File

@ -502,7 +502,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 131;
DEVELOPMENT_TEAM = S9X2AJ2US9; DEVELOPMENT_TEAM = S9X2AJ2US9;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -693,7 +693,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 131;
DEVELOPMENT_TEAM = F33K8VUZ62; DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -722,7 +722,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 131;
DEVELOPMENT_TEAM = F33K8VUZ62; DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@ -46,6 +46,9 @@ abstract class AppConfig {
/// URL /// URL
String get adminUrl; String get adminUrl;
/// Yumi管理中心URL
String get managerCenterUrl;
/// URL /// URL
String get agencyCenterUrl; String get agencyCenterUrl;

View File

@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
@override @override
String get apiHost => const String.fromEnvironment( String get apiHost => const String.fromEnvironment(
'API_HOST', 'API_HOST',
defaultValue: 'https://jvapi.haiyihy.com/', defaultValue: 'http://192.168.110.64:1100/',
); // 线 --dart-define=API_HOST http://192.168.110.64:1100/线https://jvapi.haiyihy.com/ ); // 线 --dart-define=API_HOST http://192.168.110.64:1100/线https://jvapi.haiyihy.com/
@override @override
@ -55,6 +55,10 @@ class SCVariant1Config implements AppConfig {
@override @override
String get adminUrl => 'https://h5.haiyihy.com/admin-center/index.html'; // H5 String get adminUrl => 'https://h5.haiyihy.com/admin-center/index.html'; // H5
@override
String get managerCenterUrl =>
'https://h5.haiyihy.com/hyapp/manager-center/index.html'; // H5
@override @override
String get agencyCenterUrl => String get agencyCenterUrl =>
'https://h5.haiyihy.com/agency-center/index.html'; // H5 'https://h5.haiyihy.com/agency-center/index.html'; // H5
@ -136,6 +140,7 @@ class SCVariant1Config implements AppConfig {
'imgHost': imgHost, 'imgHost': imgHost,
'privacyAgreementUrl': privacyAgreementUrl, 'privacyAgreementUrl': privacyAgreementUrl,
'userAgreementUrl': userAgreementUrl, 'userAgreementUrl': userAgreementUrl,
'managerCenterUrl': managerCenterUrl,
'primaryColor': primaryColor, 'primaryColor': primaryColor,
'tencentImAppid': tencentImAppid, 'tencentImAppid': tencentImAppid,
'agoraRtcAppid': agoraRtcAppid, 'agoraRtcAppid': agoraRtcAppid,

View File

@ -20,6 +20,7 @@ class SCGlobalConfig {
static String get bdLeaderUrl => AppConfig.current.bdLeaderUrl; static String get bdLeaderUrl => AppConfig.current.bdLeaderUrl;
static String get coinSellerUrl => AppConfig.current.coinSellerUrl; static String get coinSellerUrl => AppConfig.current.coinSellerUrl;
static String get adminUrl => AppConfig.current.adminUrl; static String get adminUrl => AppConfig.current.adminUrl;
static String get managerCenterUrl => AppConfig.current.managerCenterUrl;
static String get agencyCenterUrl => AppConfig.current.agencyCenterUrl; static String get agencyCenterUrl => AppConfig.current.agencyCenterUrl;
static String get gamesKingUrl => AppConfig.current.gamesKingUrl; static String get gamesKingUrl => AppConfig.current.gamesKingUrl;

View File

@ -1025,6 +1025,8 @@ class SCAppLocalizations {
String get adminCenter => translate('adminCenter'); String get adminCenter => translate('adminCenter');
String get managerCenter => translate('managerCenter');
String get confirm => translate('confirm'); String get confirm => translate('confirm');
String get cancel => translate('cancel'); String get cancel => translate('cancel');

View File

@ -46,7 +46,6 @@ import 'services/auth/user_profile_manager.dart';
import 'ui_kit/theme/socialchat_theme.dart'; import 'ui_kit/theme/socialchat_theme.dart';
import 'ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
import 'ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart'; import 'ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart';
import 'ui_kit/widgets/svga/sc_svga_asset_widget.dart';
bool _isCrashlyticsReady = false; bool _isCrashlyticsReady = false;
@ -109,7 +108,6 @@ void _configureImageCache() {
void _releaseVisualMemoryCaches({bool clearLiveImages = false}) { void _releaseVisualMemoryCaches({bool clearLiveImages = false}) {
SCGiftVapSvgaManager().clearMemoryCache(); SCGiftVapSvgaManager().clearMemoryCache();
SCSvgaAssetWidget.clearMemoryCache();
final imageCache = PaintingBinding.instance.imageCache; final imageCache = PaintingBinding.instance.imageCache;
imageCache.clear(); imageCache.clear();
if (clearLiveImages) { if (clearLiveImages) {

View File

@ -16,7 +16,6 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:yumi/shared/tools/sc_version_utils.dart'; import 'package:yumi/shared/tools/sc_version_utils.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
@ -376,14 +375,16 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
} }
SCLoadingManager.show(context: context); SCLoadingManager.show(context: context);
try { try {
final results = await Future.wait([ var user = await SCAccountRepository().loginForAccount(account, pass);
SCAccountRepository().loginForAccount(account, pass), if (!mounted) {
SCVersionUtils.checkReview(), return;
]); }
var user = (results[0] as SocialChatLoginRes); AccountStorage().setCurrentUser(user);
if (!mounted) { final canContinue = await SCVersionUtils.checkReview(context: context);
return; if (!mounted || !canContinue) {
} SCLoadingManager.hide();
return;
}
await Provider.of<RtcProvider>( await Provider.of<RtcProvider>(
context, context,
listen: false, listen: false,
@ -393,7 +394,6 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
if (!mounted) { if (!mounted) {
return; return;
} }
AccountStorage().setCurrentUser(user);
SCLoadingManager.hide(); SCLoadingManager.hide();
try { try {
await Future.wait([ await Future.wait([

View File

@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/main.dart' show routeObserver;
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart'; import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart';
@ -51,7 +52,7 @@ class VoiceRoomPage extends StatefulWidget {
} }
class _VoiceRoomPageState extends State<VoiceRoomPage> class _VoiceRoomPageState extends State<VoiceRoomPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, RouteAware {
static const int _chatTabCount = 3; static const int _chatTabCount = 3;
static const Duration _luckyGiftComboWindow = Duration(seconds: 3); static const Duration _luckyGiftComboWindow = Duration(seconds: 3);
static const Duration _luckyGiftQueueDrainWindow = Duration(seconds: 3); static const Duration _luckyGiftQueueDrainWindow = Duration(seconds: 3);
@ -76,11 +77,17 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
int _shownRoomStartupFailureToken = 0; int _shownRoomStartupFailureToken = 0;
RtcProvider? _rtcProvider; RtcProvider? _rtcProvider;
bool _roomProviderRebuildScheduled = false; bool _roomProviderRebuildScheduled = false;
PageRoute<dynamic>? _routeObserverRoute;
bool _roomRouteVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: _chatTabCount, vsync: this); _tabController = TabController(length: _chatTabCount, vsync: this);
Provider.of<RtcProvider>(
context,
listen: false,
).setVoiceRoomRouteVisible(true);
_enableRoomVisualEffects(); _enableRoomVisualEffects();
_tabController.addListener(_handleTabChange); _tabController.addListener(_handleTabChange);
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen(( _subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
@ -110,11 +117,23 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
rtcProvider.addListener(_handleRoomProviderChanged); rtcProvider.addListener(_handleRoomProviderChanged);
context.read<RoomMusicManager>().attachRtcProvider(rtcProvider); context.read<RoomMusicManager>().attachRtcProvider(rtcProvider);
} }
final route = ModalRoute.of(context);
if (route is PageRoute<dynamic> && _routeObserverRoute != route) {
if (_routeObserverRoute != null) {
routeObserver.unsubscribe(this);
}
_routeObserverRoute = route;
routeObserver.subscribe(this, route);
_setRoomRouteVisible(route.isCurrent);
}
_ensureRoomVisualEffectsEnabled(); _ensureRoomVisualEffectsEnabled();
} }
@override @override
void dispose() { void dispose() {
routeObserver.unsubscribe(this);
_routeObserverRoute = null;
_setRoomRouteVisible(false);
_rtcProvider?.removeListener(_handleRoomProviderChanged); _rtcProvider?.removeListener(_handleRoomProviderChanged);
_rtcProvider = null; _rtcProvider = null;
_roomProviderRebuildScheduled = false; _roomProviderRebuildScheduled = false;
@ -125,6 +144,36 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
super.dispose(); super.dispose();
} }
@override
void didPush() {
_setRoomRouteVisible(true);
}
@override
void didPopNext() {
_setRoomRouteVisible(true);
}
@override
void didPushNext() {
_setRoomRouteVisible(false);
}
@override
void didPop() {
_setRoomRouteVisible(false);
}
void _setRoomRouteVisible(bool visible) {
if (_roomRouteVisible == visible) {
return;
}
_roomRouteVisible = visible;
final rtcProvider =
_rtcProvider ?? Provider.of<RtcProvider>(context, listen: false);
rtcProvider.setVoiceRoomRouteVisible(visible);
}
void _handleTabChange() { void _handleTabChange() {
if (!mounted) { if (!mounted) {
return; return;
@ -168,6 +217,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
void _suspendRoomVisualEffects() { void _suspendRoomVisualEffects() {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false); final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
rtcProvider.setVoiceRoomRouteVisible(false);
rtcProvider.setRoomVisualEffectsEnabled(false); rtcProvider.setRoomVisualEffectsEnabled(false);
final rtmProvider = Provider.of<RtmProvider>(context, listen: false); final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
@ -184,7 +236,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
_clearGiftFlightBatchDedupeTimers(); _clearGiftFlightBatchDedupeTimers();
_clearGiftVisualBatchDedupeTimers(); _clearGiftVisualBatchDedupeTimers();
_giftSeatFlightController.clear(); _giftSeatFlightController.clear();
OverlayManager().removeRoom(); OverlayManager().removeRoom(roomId: currentRoomId);
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend'); SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
SCGiftVapSvgaManager().stopPlayback(); SCGiftVapSvgaManager().stopPlayback();
unawaited( unawaited(

View File

@ -79,13 +79,15 @@ class VoiceRoomRoute implements SCIRouterProvider {
final floatingGiftListener = rtmProvider.msgFloatingGiftListener; final floatingGiftListener = rtmProvider.msgFloatingGiftListener;
final luckyGiftRewardTickerListener = final luckyGiftRewardTickerListener =
rtmProvider.msgLuckyGiftRewardTickerListener; rtmProvider.msgLuckyGiftRewardTickerListener;
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
rtcProvider.setRoomVisualEffectsEnabled(false); rtcProvider.setRoomVisualEffectsEnabled(false);
rtmProvider.msgFloatingGiftListener = null; rtmProvider.msgFloatingGiftListener = null;
rtmProvider.msgLuckyGiftRewardTickerListener = null; rtmProvider.msgLuckyGiftRewardTickerListener = null;
RoomEntranceHelper.clearQueue(); RoomEntranceHelper.clearQueue();
giftAnimationManager.clearActiveAnimations(); giftAnimationManager.clearActiveAnimations();
OverlayManager().removeRoom(); OverlayManager().removeRoom(roomId: currentRoomId);
SCRoomEffectScheduler().clearDeferredTasks(reason: 'room_background_route'); SCRoomEffectScheduler().clearDeferredTasks(reason: 'room_background_route');
SCGiftVapSvgaManager().stopPlayback(); SCGiftVapSvgaManager().stopPlayback();

View File

@ -448,13 +448,13 @@ class _SplashPageState extends State<SplashPage> {
Future<void> _goMainPage() async { Future<void> _goMainPage() async {
try { try {
await SCVersionUtils.checkReview();
if (!mounted) {
return;
}
var user = AccountStorage().getCurrentUser(); var user = AccountStorage().getCurrentUser();
var token = AccountStorage().getToken(); var token = AccountStorage().getToken();
if (user != null && token.isNotEmpty) { if (user != null && token.isNotEmpty) {
final canContinue = await SCVersionUtils.checkReview(context: context);
if (!mounted || !canContinue) {
return;
}
SCNavigatorUtils.push(context, SCRoutes.home, replace: true); SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
return; return;
} }
@ -464,6 +464,10 @@ class _SplashPageState extends State<SplashPage> {
} }
if (restoredUser != null && if (restoredUser != null &&
(restoredUser.token ?? "").trim().isNotEmpty) { (restoredUser.token ?? "").trim().isNotEmpty) {
final canContinue = await SCVersionUtils.checkReview(context: context);
if (!mounted || !canContinue) {
return;
}
SCNavigatorUtils.push(context, SCRoutes.home, replace: true); SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
} else { } else {
SCNavigatorUtils.pushLoginIfNeeded(context, replace: true); SCNavigatorUtils.pushLoginIfNeeded(context, replace: true);

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
@ -50,6 +51,7 @@ class _MePage2State extends State<MePage2> {
listen: false, listen: false,
); );
profileManager.fetchUserProfileData(); profileManager.fetchUserProfileData();
profileManager.getUserIdentity();
profileManager.balance(); profileManager.balance();
_loadCounter(); _loadCounter();
_loadVipStatus(); _loadVipStatus();
@ -660,6 +662,16 @@ class _MePage2State extends State<MePage2> {
final items = <_MenuRowData>[]; final items = <_MenuRowData>[];
final hasCurrentUser = userProfile.currentUserProfile != null; final hasCurrentUser = userProfile.currentUserProfile != null;
final isAgent = userProfile.userIdentity?.agent ?? false; final isAgent = userProfile.userIdentity?.agent ?? false;
final isYumiManager = userProfile.userIdentity?.yumiManager ?? false;
if (kDebugMode) {
final currentProfile = userProfile.currentUserProfile;
debugPrint(
'[MePage2] menu identity userId=${currentProfile?.id}, '
'account=${currentProfile?.account}, hasCurrentUser=$hasCurrentUser, '
'agent=${userProfile.userIdentity?.agent}, admin=${userProfile.userIdentity?.admin}, '
'manager=${userProfile.userIdentity?.manager}, yumiManager=${userProfile.userIdentity?.yumiManager}',
);
}
// 1. / // 1. /
if (hasCurrentUser) { if (hasCurrentUser) {
@ -754,6 +766,25 @@ class _MePage2State extends State<MePage2> {
), ),
); );
} }
if (isYumiManager) {
if (kDebugMode) {
debugPrint('[MePage2] show Manager Center');
}
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.managerCenter,
assetIcon: 'sc_images/index/sc_icon_admin_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.managerCenterUrl)}&showTitle=false",
);
},
),
);
} else if (kDebugMode) {
debugPrint('[MePage2] hide Manager Center because yumiManager=$isYumiManager');
}
return _buildMenuCard(items); return _buildMenuCard(items);
} }

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svga/flutter_svga.dart'; import 'package:flutter_svga/flutter_svga.dart';
@ -564,7 +566,6 @@ class _GiftWallSvgaEffectOverlay extends StatefulWidget {
class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller; late final SVGAAnimationController _controller;
MovieEntity? _retainedMovieEntity;
bool _loaded = false; bool _loaded = false;
bool _finished = false; bool _finished = false;
@ -584,15 +585,9 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
Future<void> _loadAndPlay() async { Future<void> _loadAndPlay() async {
try { try {
final movieEntity = await SCGiftVapSvgaManager().acquireSvgaEntity( final movieEntity = await _loadMovieEntity(widget.sourceUrl);
widget.sourceUrl, if (!mounted || _finished) return;
);
if (!mounted || _finished) {
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
return;
}
setState(() { setState(() {
_retainedMovieEntity = movieEntity;
_loaded = true; _loaded = true;
_controller.videoItem = movieEntity; _controller.videoItem = movieEntity;
}); });
@ -604,6 +599,28 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
} }
} }
Future<MovieEntity> _loadMovieEntity(String sourceUrl) async {
final cache = SCGiftVapSvgaManager().videoItemCache;
final cached = cache[sourceUrl];
if (cached != null) return cached;
final pathType = SCPathUtils.getPathType(sourceUrl);
late final MovieEntity movieEntity;
if (pathType == PathType.network) {
movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl);
} else if (pathType == PathType.asset) {
movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl);
} else if (pathType == PathType.file) {
final bytes = await File(sourceUrl).readAsBytes();
movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes);
} else {
throw Exception("Unsupported SVGA source: $sourceUrl");
}
movieEntity.autorelease = false;
cache[sourceUrl] = movieEntity;
return movieEntity;
}
void _finish() { void _finish() {
if (_finished) return; if (_finished) return;
_finished = true; _finished = true;
@ -614,8 +631,6 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
void dispose() { void dispose() {
_controller.removeStatusListener(_handleStatusChanged); _controller.removeStatusListener(_handleStatusChanged);
_controller.dispose(); _controller.dispose();
SCGiftVapSvgaManager().releaseSvgaEntity(_retainedMovieEntity);
_retainedMovieEntity = null;
super.dispose(); super.dispose();
} }

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -48,6 +49,9 @@ class _WebViewPageState extends State<WebViewPage> {
void initState() { void initState() {
super.initState(); super.initState();
urlLink = SCH5UrlUtils.appendAppParams(widget.url); urlLink = SCH5UrlUtils.appendAppParams(widget.url);
if (kDebugMode) {
debugPrint('[WebViewPage] load url=${_maskSensitiveUrl(urlLink)}');
}
// WebViewController // WebViewController
_controller = _controller =
WebViewController() WebViewController()
@ -138,11 +142,21 @@ class _WebViewPageState extends State<WebViewPage> {
}); });
}, },
onPageStarted: (String url) { onPageStarted: (String url) {
if (kDebugMode) {
debugPrint(
'[WebViewPage] page started url=${_maskSensitiveUrl(url)}',
);
}
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
}, },
onPageFinished: (String url) { onPageFinished: (String url) {
if (kDebugMode) {
debugPrint(
'[WebViewPage] page finished url=${_maskSensitiveUrl(url)}',
);
}
// 500Vue3初始化 // 500Vue3初始化
Future.delayed(Duration(milliseconds: 550), () { Future.delayed(Duration(milliseconds: 550), () {
_injectJavaScriptInterface(); _injectJavaScriptInterface();
@ -310,6 +324,23 @@ class _WebViewPageState extends State<WebViewPage> {
} }
void _onWebResourceError(WebResourceError error) { void _onWebResourceError(WebResourceError error) {
if (kDebugMode) {
debugPrint(
'[WebViewPage] resource error code=${error.errorCode}, '
'type=${error.errorType}, url=${_maskSensitiveUrl(error.url ?? '')}, '
'description=${error.description}',
);
}
// //
} }
String _maskSensitiveUrl(String url) {
final uri = Uri.tryParse(url);
if (uri == null) return url;
final params = Map<String, String>.from(uri.queryParameters);
if (params.containsKey('token')) {
params['token'] = '***';
}
return uri.replace(queryParameters: params).toString();
}
} }

View File

@ -311,6 +311,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool needUpDataUserInfo = false; bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false; bool _roomVisualEffectsEnabled = false;
bool _voiceRoomRouteVisible = false;
bool _isExitingCurrentVoiceRoomSession = false; bool _isExitingCurrentVoiceRoomSession = false;
bool _isHandlingAgoraRoomFailure = false; bool _isHandlingAgoraRoomFailure = false;
bool _agoraJoined = false; bool _agoraJoined = false;
@ -435,6 +436,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
bool get isVoiceRoomRouteVisible => _voiceRoomRouteVisible;
bool get isExitingCurrentVoiceRoomSession => bool get isExitingCurrentVoiceRoomSession =>
_isExitingCurrentVoiceRoomSession; _isExitingCurrentVoiceRoomSession;
@ -479,6 +482,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setVoiceRoomRouteVisible(bool visible) {
_voiceRoomRouteVisible = visible;
}
void _setRoomStartupLoading() { void _setRoomStartupLoading() {
_isHandlingRoomStartupFailure = false; _isHandlingRoomStartupFailure = false;
_roomStartupFailureType = RoomStartupFailureType.none; _roomStartupFailureType = RoomStartupFailureType.none;
@ -3383,6 +3390,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
redPacketList.clear(); redPacketList.clear();
currenRoom = null; currenRoom = null;
_roomVisualEffectsEnabled = false; _roomVisualEffectsEnabled = false;
_voiceRoomRouteVisible = false;
_isExitingCurrentVoiceRoomSession = false; _isExitingCurrentVoiceRoomSession = false;
isMic = true; isMic = true;
isMusicPlaying = false; isMusicPlaying = false;

View File

@ -1697,6 +1697,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
final currentRoomId = final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
return rtcProvider.shouldShowRoomVisualEffects && return rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible &&
currentRoomId.isNotEmpty && currentRoomId.isNotEmpty &&
currentRoomId == normalizedRoomId; currentRoomId == normalizedRoomId;
} }

View File

@ -10,7 +10,6 @@ import 'package:yumi/shared/tools/sc_version_utils.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
@ -64,12 +63,22 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
SCLoadingManager.show(); SCLoadingManager.show();
if (uid.isNotEmpty) { if (uid.isNotEmpty) {
await DataPersistence.setPendingChannelAuth(authType, uid); await DataPersistence.setPendingChannelAuth(authType, uid);
final res = await Future.wait([ var user = await SCAccountRepository().loginForChannel(authType, uid);
SCAccountRepository().loginForChannel(authType, uid),
SCVersionUtils.checkReview(),
]);
var user = (res[0] as SocialChatLoginRes);
if (!context.mounted) { if (!context.mounted) {
SCLoadingManager.hide();
return;
}
AccountStorage().setCurrentUser(user);
await DataPersistence.clearPendingChannelAuth();
if (!context.mounted) {
SCLoadingManager.hide();
return;
}
final canContinue = await SCVersionUtils.checkReview(
context: context,
);
if (!context.mounted || !canContinue) {
SCLoadingManager.hide();
return; return;
} }
await Provider.of<RtcProvider>( await Provider.of<RtcProvider>(
@ -84,11 +93,6 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
AccountStorage().setCurrentUser(user);
await DataPersistence.clearPendingChannelAuth();
if (!context.mounted) {
return;
}
SCLoadingManager.hide(); SCLoadingManager.hide();
notifyListeners(); notifyListeners();
SCNavigatorUtils.push(context, SCRoutes.home, replace: true); SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
@ -109,12 +113,22 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
SCLoadingManager.show(); SCLoadingManager.show();
if (uid.isNotEmpty) { if (uid.isNotEmpty) {
await DataPersistence.setPendingChannelAuth(authType, uid); await DataPersistence.setPendingChannelAuth(authType, uid);
final res = await Future.wait([ var user = await SCAccountRepository().loginForChannel(authType, uid);
SCAccountRepository().loginForChannel(authType, uid),
SCVersionUtils.checkReview(),
]);
var user = (res[0] as SocialChatLoginRes);
if (!context.mounted) { if (!context.mounted) {
SCLoadingManager.hide();
return;
}
AccountStorage().setCurrentUser(user);
await DataPersistence.clearPendingChannelAuth();
if (!context.mounted) {
SCLoadingManager.hide();
return;
}
final canContinue = await SCVersionUtils.checkReview(
context: context,
);
if (!context.mounted || !canContinue) {
SCLoadingManager.hide();
return; return;
} }
await Provider.of<RtcProvider>( await Provider.of<RtcProvider>(
@ -129,11 +143,6 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
AccountStorage().setCurrentUser(user);
await DataPersistence.clearPendingChannelAuth();
if (!context.mounted) {
return;
}
SCLoadingManager.hide(); SCLoadingManager.hide();
notifyListeners(); notifyListeners();
SCNavigatorUtils.push(context, SCRoutes.home, replace: true); SCNavigatorUtils.push(context, SCRoutes.home, replace: true);

View File

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
@ -95,8 +96,26 @@ class SocialChatUserProfileManager extends ChangeNotifier {
SCUserIdentityRes? userIdentity; SCUserIdentityRes? userIdentity;
void getUserIdentity() async { void getUserIdentity() async {
userIdentity = await SCAccountRepository().userIdentity(); if (kDebugMode) {
notifyListeners(); final profile = AccountStorage().getCurrentUser()?.userProfile;
debugPrint(
'[SCUserIdentity] request start userId=${profile?.id}, account=${profile?.account}',
);
}
try {
userIdentity = await SCAccountRepository().userIdentity();
if (kDebugMode) {
debugPrint(
'[SCUserIdentity] manager stored admin=${userIdentity?.admin}, '
'manager=${userIdentity?.manager}, yumiManager=${userIdentity?.yumiManager}',
);
}
notifyListeners();
} catch (error) {
if (kDebugMode) {
debugPrint('[SCUserIdentity] request failed error=$error');
}
}
} }
/// Me /// Me

View File

@ -1,7 +1,7 @@
/// agent : true // agent : true
/// anchor : true // anchor : true
/// bd : false // bd : false
/// freightAgent : false // freightAgent : false
class SCUserIdentityRes { class SCUserIdentityRes {
SCUserIdentityRes({ SCUserIdentityRes({
@ -13,12 +13,14 @@ class SCUserIdentityRes {
bool? bdLeader, bool? bdLeader,
bool? superFreightAgent, bool? superFreightAgent,
bool? manager, bool? manager,
bool? yumiManager,
bool? freightAgent,}){ bool? freightAgent,}){
_agent = agent; _agent = agent;
_superFreightAgent = superFreightAgent; _superFreightAgent = superFreightAgent;
_anchor = anchor; _anchor = anchor;
_bd = bd; _bd = bd;
_manager = manager; _manager = manager;
_yumiManager = yumiManager;
_admin = admin; _admin = admin;
_superAdmin = superAdmin; _superAdmin = superAdmin;
_bdLeader = bdLeader; _bdLeader = bdLeader;
@ -35,6 +37,9 @@ class SCUserIdentityRes {
_admin = json['admin']; _admin = json['admin'];
_freightAgent = json['freightAgent']; _freightAgent = json['freightAgent'];
_manager = json['manager']; _manager = json['manager'];
_yumiManager = _boolValue(
json['yumi_manager'] ?? json['yumiManager'] ?? json['manager'],
);
} }
bool? _agent; bool? _agent;
bool? _anchor; bool? _anchor;
@ -44,13 +49,16 @@ class SCUserIdentityRes {
bool? _bdLeader; bool? _bdLeader;
bool? _freightAgent; bool? _freightAgent;
bool? _manager; bool? _manager;
bool? _yumiManager;
bool? _superFreightAgent; bool? _superFreightAgent;
SCUserIdentityRes copyWith({ bool? agent, SCUserIdentityRes copyWith({ bool? agent,
bool? anchor, bool? anchor,
bool? bd, bool? bd,
bool? admin, bool? admin,
bool? superAdmin, bool? superAdmin,
bool? bdLeader,
bool? manager, bool? manager,
bool? yumiManager,
bool? superFreightAgent, bool? superFreightAgent,
bool? freightAgent, bool? freightAgent,
}) => SCUserIdentityRes( agent: agent ?? _agent, }) => SCUserIdentityRes( agent: agent ?? _agent,
@ -60,6 +68,7 @@ SCUserIdentityRes copyWith({ bool? agent,
bdLeader: bdLeader ?? _bdLeader, bdLeader: bdLeader ?? _bdLeader,
admin: admin ?? _admin, admin: admin ?? _admin,
manager: manager ?? _manager, manager: manager ?? _manager,
yumiManager: yumiManager ?? _yumiManager,
superFreightAgent: superFreightAgent ?? _superFreightAgent, superFreightAgent: superFreightAgent ?? _superFreightAgent,
freightAgent: freightAgent ?? _freightAgent, freightAgent: freightAgent ?? _freightAgent,
); );
@ -68,6 +77,7 @@ SCUserIdentityRes copyWith({ bool? agent,
bool? get bd => _bd; bool? get bd => _bd;
bool? get admin => _admin; bool? get admin => _admin;
bool? get manager => _manager; bool? get manager => _manager;
bool? get yumiManager => _yumiManager;
bool? get superAdmin => _superAdmin; bool? get superAdmin => _superAdmin;
bool? get bdLeader => _bdLeader; bool? get bdLeader => _bdLeader;
bool? get superFreightAgent => _superFreightAgent; bool? get superFreightAgent => _superFreightAgent;
@ -79,6 +89,7 @@ SCUserIdentityRes copyWith({ bool? agent,
map['anchor'] = _anchor; map['anchor'] = _anchor;
map['bd'] = _bd; map['bd'] = _bd;
map['manager'] = _manager; map['manager'] = _manager;
map['yumi_manager'] = _yumiManager;
map['admin'] = _admin; map['admin'] = _admin;
map['superAdmin'] = _superAdmin; map['superAdmin'] = _superAdmin;
map['bdLeader'] = _bdLeader; map['bdLeader'] = _bdLeader;
@ -87,4 +98,25 @@ SCUserIdentityRes copyWith({ bool? agent,
return map; return map;
} }
} static bool? _boolValue(dynamic value) {
if (value == null || value is bool) {
return value;
}
if (value is num) {
return value != 0;
}
if (value is String) {
final normalized = value.trim().toLowerCase();
if (normalized.isEmpty) {
return null;
}
if (normalized == 'true' || normalized == '1' || normalized == 'yes') {
return true;
}
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
return false;
}
}
return null;
}
}

View File

@ -470,15 +470,23 @@ class OverlayManager {
_isProcessing = false; _isProcessing = false;
} }
void removeRoom() { void removeRoom({String? roomId}) {
_removeActiveRoomMessage(); final normalizedRoomId = roomId?.trim() ?? '';
_removeActiveRoomMessage(
roomId: normalizedRoomId,
includeRedPacket: normalizedRoomId.isNotEmpty,
);
_removeMessagesByType(1); _removeMessagesByType(1);
_removeMessagesByType(0); _removeMessagesByType(0);
_removeMessagesByType(5); _removeMessagesByType(5);
if (normalizedRoomId.isNotEmpty) {
_removeMessagesByType(4, roomId: normalizedRoomId);
}
} }
// //
void _removeMessagesByType(int type) { void _removeMessagesByType(int type, {String? roomId}) {
final normalizedRoomId = roomId?.trim() ?? '';
// PriorityQueue // PriorityQueue
// //
final newQueue = SCPriorityQueue<SCFloatingMessage>( final newQueue = SCPriorityQueue<SCFloatingMessage>(
@ -488,7 +496,11 @@ class OverlayManager {
// //
while (_messageQueue.isNotEmpty) { while (_messageQueue.isNotEmpty) {
final message = _messageQueue.removeFirst(); final message = _messageQueue.removeFirst();
if (message.type != type) { final matchesType = message.type == type;
final matchesRoom =
normalizedRoomId.isEmpty ||
(message.roomId ?? "").trim() == normalizedRoomId;
if (!matchesType || !matchesRoom) {
newQueue.add(message); newQueue.add(message);
} }
} }
@ -605,7 +617,8 @@ class OverlayManager {
context, context,
listen: false, listen: false,
); );
if (!rtcProvider.shouldShowRoomVisualEffects) { if (!rtcProvider.shouldShowRoomVisualEffects ||
!rtcProvider.isVoiceRoomRouteVisible) {
return false; return false;
} }
final currentRoomId = final currentRoomId =
@ -633,6 +646,7 @@ class OverlayManager {
final messageRoomId = (message.roomId ?? "").trim(); final messageRoomId = (message.roomId ?? "").trim();
final isCurrentVisibleRoom = final isCurrentVisibleRoom =
rtcProvider.shouldShowRoomVisualEffects && rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible &&
currentRoomId.isNotEmpty && currentRoomId.isNotEmpty &&
messageRoomId.isNotEmpty && messageRoomId.isNotEmpty &&
currentRoomId == messageRoomId; currentRoomId == messageRoomId;
@ -640,6 +654,13 @@ class OverlayManager {
// RTM IM // RTM IM
return message.type == 4; return message.type == 4;
} }
final isCurrentRoomMessage =
currentRoomId.isNotEmpty &&
messageRoomId.isNotEmpty &&
currentRoomId == messageRoomId;
if (message.type == 4 && isCurrentRoomMessage) {
return false;
}
if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) { if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) {
return true; return true;
} }
@ -684,12 +705,21 @@ class OverlayManager {
_safeScheduleNext(); _safeScheduleNext();
} }
void _removeActiveRoomMessage() { void _removeActiveRoomMessage({
String? roomId,
bool includeRedPacket = false,
}) {
final activeMessage = _currentMessage; final activeMessage = _currentMessage;
final normalizedRoomId = roomId?.trim() ?? '';
if (activeMessage == null || if (activeMessage == null ||
(activeMessage.type != 0 && (activeMessage.type != 0 &&
activeMessage.type != 1 && activeMessage.type != 1 &&
activeMessage.type != 5)) { activeMessage.type != 5 &&
!(includeRedPacket && activeMessage.type == 4))) {
return;
}
if (normalizedRoomId.isNotEmpty &&
(activeMessage.roomId ?? "").trim() != normalizedRoomId) {
return; return;
} }
_removeActiveMessage(); _removeActiveMessage();

View File

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
@ -296,7 +298,10 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
Future<SCVersionManageLatestRes> versionManageLatest() async { Future<SCVersionManageLatestRes> versionManageLatest() async {
final result = await http.get<SCVersionManageLatestRes>( final result = await http.get<SCVersionManageLatestRes>(
"ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687",
fromJson: (json) => SCVersionManageLatestRes.fromJson(json), fromJson: (json) {
_debugVersionResponse('releaseLatest', json);
return SCVersionManageLatestRes.fromJson(json);
},
); );
return result; return result;
} }
@ -306,11 +311,22 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
Future<VersionManageLatesReviewRes> versionManageLatestReview() async { Future<VersionManageLatesReviewRes> versionManageLatestReview() async {
final result = await http.get<VersionManageLatesReviewRes>( final result = await http.get<VersionManageLatesReviewRes>(
"ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232",
fromJson: (json) => VersionManageLatesReviewRes.fromJson(json), fromJson: (json) {
_debugVersionResponse('latestReview', json);
return VersionManageLatesReviewRes.fromJson(json);
},
); );
return result; return result;
} }
void _debugVersionResponse(String name, dynamic json) {
try {
debugPrint('[SCVersionUtils] $name rawBody=${jsonEncode(json)}');
} catch (e) {
debugPrint('[SCVersionUtils] $name rawBody=$json encodeError=$e');
}
}
///sys/config/customer-service ///sys/config/customer-service
@override @override
Future<SocialChatUserProfile> customerService() async { Future<SocialChatUserProfile> customerService() async {

View File

@ -607,7 +607,20 @@ class SCAccountRepository implements SocialChatUserRepository {
final result = await http.get<SCUserIdentityRes>( final result = await http.get<SCUserIdentityRes>(
"d29121f09fa26c45b5322df6de0fc25b", "d29121f09fa26c45b5322df6de0fc25b",
queryParams: parm, queryParams: parm,
fromJson: (json) => SCUserIdentityRes.fromJson(json), fromJson: (json) {
final identity = SCUserIdentityRes.fromJson(json);
if (kDebugMode) {
debugPrint('[SCUserIdentity] raw=${jsonEncode(json)}');
debugPrint(
'[SCUserIdentity] parsed admin=${identity.admin}, '
'manager=${identity.manager}, yumiManager=${identity.yumiManager}, '
'raw_manager=${json['manager']}(${json['manager']?.runtimeType}), '
'raw_yumi_manager=${json['yumi_manager']}(${json['yumi_manager']?.runtimeType}), '
'raw_yumiManager=${json['yumiManager']}(${json['yumiManager']?.runtimeType})',
);
}
return identity;
},
); );
return result; return result;
} }

View File

@ -23,8 +23,7 @@ class SCGiftVapSvgaManager {
static const int _maxConsecutiveEntryTasksBeforeGift = 2; static const int _maxConsecutiveEntryTasksBeforeGift = 2;
static const Duration _taskWatchdogTimeout = Duration(seconds: 20); static const Duration _taskWatchdogTimeout = Duration(seconds: 20);
static const Duration _entryTaskTtl = Duration(seconds: 6); static const Duration _entryTaskTtl = Duration(seconds: 6);
final Map<String, MovieEntity> _videoItemCache = <String, MovieEntity>{}; Map<String, MovieEntity> videoItemCache = {};
final Map<MovieEntity, int> _svgaEntityUseCounts = <MovieEntity, int>{};
static SCGiftVapSvgaManager? _inst; static SCGiftVapSvgaManager? _inst;
static const int _maxPreloadConcurrency = 1; static const int _maxPreloadConcurrency = 1;
static const int _maxPreloadQueueLength = 12; static const int _maxPreloadQueueLength = 12;
@ -80,44 +79,6 @@ class SCGiftVapSvgaManager {
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
} }
Future<MovieEntity> acquireSvgaEntity(String path) async {
final entity = await _loadSvgaEntity(path);
_retainSvgaEntity(entity);
return entity;
}
void releaseSvgaEntity(MovieEntity? entity) {
if (entity == null) {
return;
}
final count = _svgaEntityUseCounts[entity] ?? 0;
if (count <= 1) {
_svgaEntityUseCounts.remove(entity);
if (!_videoItemCache.containsValue(entity) &&
!_isSvgaEntityInUse(entity)) {
_disposeSvgaEntity(entity);
} else {
_trimSvgaCache();
}
return;
}
_svgaEntityUseCounts[entity] = count - 1;
}
void evictSvgaEntity(String path, {bool includeCurrent = false}) {
final entity = _videoItemCache.remove(path);
if (entity == null) {
return;
}
if (includeCurrent || !_isSvgaEntityInUse(entity)) {
_disposeSvgaEntity(entity);
}
}
void _retainSvgaEntity(MovieEntity entity) {
_svgaEntityUseCounts[entity] = (_svgaEntityUseCounts[entity] ?? 0) + 1;
}
static int resolveEffectPriority({ static int resolveEffectPriority({
int priority = 0, int priority = 0,
int type = giftEffectType, int type = giftEffectType,
@ -155,7 +116,7 @@ class SCGiftVapSvgaManager {
bool _isPreloadedOrLoading(String path) { bool _isPreloadedOrLoading(String path) {
if (_needsSvgaController(path)) { if (_needsSvgaController(path)) {
return _videoItemCache.containsKey(path) || return videoItemCache.containsKey(path) ||
_svgaLoadTasks.containsKey(path); _svgaLoadTasks.containsKey(path);
} }
final pathType = SCPathUtils.getPathType(path); final pathType = SCPathUtils.getPathType(path);
@ -295,19 +256,19 @@ class SCGiftVapSvgaManager {
} }
MovieEntity? _touchCachedSvgaEntity(String path) { MovieEntity? _touchCachedSvgaEntity(String path) {
final cached = _videoItemCache.remove(path); final cached = videoItemCache.remove(path);
if (cached != null) { if (cached != null) {
_videoItemCache[path] = cached; videoItemCache[path] = cached;
} }
return cached; return cached;
} }
void _cacheSvgaEntity(String path, MovieEntity entity) { void _cacheSvgaEntity(String path, MovieEntity entity) {
final previous = _videoItemCache.remove(path); final previous = videoItemCache.remove(path);
if (previous != null && !identical(previous, entity)) { if (previous != null && !identical(previous, entity)) {
_disposeSvgaEntityIfUnused(previous); _disposeSvgaEntityIfUnused(previous);
} }
_videoItemCache[path] = entity; videoItemCache[path] = entity;
_trimSvgaCache(); _trimSvgaCache();
} }
@ -326,17 +287,17 @@ class SCGiftVapSvgaManager {
} }
void _trimSvgaCache() { void _trimSvgaCache() {
while (_videoItemCache.length > _maxSvgaCacheEntries) { while (videoItemCache.length > _maxSvgaCacheEntries) {
final removableEntry = _videoItemCache.entries.firstWhere( final removableEntry = videoItemCache.entries.firstWhere(
(entry) => !_isSvgaEntityInUse(entry.value), (entry) => !_isSvgaEntityInUse(entry.value),
orElse: () => _videoItemCache.entries.first, orElse: () => videoItemCache.entries.first,
); );
if (_isSvgaEntityInUse(removableEntry.value)) { if (_isSvgaEntityInUse(removableEntry.value)) {
_videoItemCache.remove(removableEntry.key); videoItemCache.remove(removableEntry.key);
_videoItemCache[removableEntry.key] = removableEntry.value; videoItemCache[removableEntry.key] = removableEntry.value;
break; break;
} }
_videoItemCache.remove(removableEntry.key); videoItemCache.remove(removableEntry.key);
_disposeSvgaEntity(removableEntry.value); _disposeSvgaEntity(removableEntry.value);
} }
} }
@ -791,21 +752,20 @@ class SCGiftVapSvgaManager {
_preloadQueue.clear(); _preloadQueue.clear();
_queuedPreloadPaths.clear(); _queuedPreloadPaths.clear();
_activePreloadCount = 0; _activePreloadCount = 0;
final entries = _videoItemCache.entries.toList(growable: false); final entries = videoItemCache.entries.toList(growable: false);
for (final entry in entries) { for (final entry in entries) {
final entity = entry.value; final entity = entry.value;
if (!includeCurrent && _isSvgaEntityInUse(entity)) { if (!includeCurrent && _isSvgaEntityInUse(entity)) {
continue; continue;
} }
_videoItemCache.remove(entry.key); videoItemCache.remove(entry.key);
_disposeSvgaEntity(entity); _disposeSvgaEntity(entity);
} }
_playablePathCache.clear(); _playablePathCache.clear();
} }
bool _isSvgaEntityInUse(MovieEntity entity) { bool _isSvgaEntityInUse(MovieEntity entity) {
return identical(_rsc?.videoItem, entity) || return identical(_rsc?.videoItem, entity);
(_svgaEntityUseCounts[entity] ?? 0) > 0;
} }
void _disposeSvgaEntityIfUnused(MovieEntity entity) { void _disposeSvgaEntityIfUnused(MovieEntity entity) {
@ -817,7 +777,6 @@ class SCGiftVapSvgaManager {
void _disposeSvgaEntity(MovieEntity entity) { void _disposeSvgaEntity(MovieEntity entity) {
try { try {
_svgaEntityUseCounts.remove(entity);
entity.dispose(); entity.dispose();
} catch (_) {} } catch (_) {}
} }

View File

@ -1,14 +1,599 @@
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; import 'dart:async';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'dart:io';
class SCVersionUtils { import 'package:flutter/material.dart';
static Future<void> checkReview() async { import 'package:yumi/app/constants/sc_global_config.dart';
try { import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart';
var versionInfo = await SCConfigRepositoryImp().versionManageLatestReview(); import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
if (versionInfo.review != null) { import 'package:yumi/shared/tools/sc_loading_manager.dart';
SCGlobalConfig.isReview = import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
"${versionInfo.review?.version}" == SCGlobalConfig.version;
} class SCVersionUtils {
} catch (e) {} static bool _forceUpdateDialogShowing = false;
}
} static Future<bool> checkReview({BuildContext? context}) async {
_debugLog(
'checkReview start '
'currentVersion=${SCGlobalConfig.version} '
'currentBuild=${SCGlobalConfig.build} '
'platform=${Platform.isIOS ? "ios" : "android"} '
'apiHost=${SCGlobalConfig.apiHost} '
'hasContext=${context != null} '
'contextMounted=${context?.mounted}',
);
try {
var versionInfo =
await SCConfigRepositoryImp().versionManageLatestReview();
_debugLog(
'checkReview response '
'review=${_versionInfoDebug(versionInfo.review)} '
'latest=${_versionInfoDebug(versionInfo.latest)}',
);
if (versionInfo.review != null) {
SCGlobalConfig.isReview =
"${versionInfo.review?.version}" == SCGlobalConfig.version;
_debugLog('checkReview isReview=${SCGlobalConfig.isReview}');
}
final latest = await _resolveLatestVersion(versionInfo.latest);
final shouldForceUpdate = latest != null && _shouldForceUpdate(latest);
_debugLog(
'checkReview forceDecision '
'shouldForceUpdate=$shouldForceUpdate '
'reason=${_forceUpdateDecisionDebug(latest)}',
);
if (context != null &&
context.mounted &&
latest != null &&
shouldForceUpdate) {
SCLoadingManager.hide();
_debugLog('checkReview showForceUpdateDialog');
_showForceUpdateDialog(context, latest);
return false;
}
if (shouldForceUpdate && context == null) {
_debugLog('checkReview skip dialog reason=no_context');
} else if (shouldForceUpdate &&
context != null &&
context.mounted == false) {
_debugLog('checkReview skip dialog reason=context_not_mounted');
} else {
_debugLog('checkReview no force dialog');
}
} catch (e) {
_debugLog('checkReview request failed error=$e');
return true;
}
return true;
}
static bool _shouldForceUpdate(SCVersionManageLatestRes latest) {
if (latest.forceUpdate != true) {
return false;
}
final latestBuild = latest.buildVersion?.toInt() ?? 0;
final currentBuild = _currentComparableBuild();
final hasLatestVersion = (latest.version ?? '').trim().isNotEmpty;
final versionCompare = _compareVersion(
latest.version,
SCGlobalConfig.version,
);
if (hasLatestVersion) {
if (versionCompare > 0) {
return true;
}
if (versionCompare < 0) {
return false;
}
}
if (latestBuild > 0 && currentBuild > 0) {
return latestBuild > currentBuild;
}
return hasLatestVersion && versionCompare > 0;
}
static Future<SCVersionManageLatestRes?> _resolveLatestVersion(
SCVersionManageLatestRes? latest,
) async {
if (!_isVersionInfoEmpty(latest)) {
return latest;
}
_debugLog('checkReview latestReview latest empty, fallback releaseLatest');
try {
final fallback = await SCConfigRepositoryImp().versionManageLatest();
_debugLog('checkReview fallbackLatest=${_versionInfoDebug(fallback)}');
return _isVersionInfoEmpty(fallback) ? latest : fallback;
} catch (e) {
_debugLog('checkReview fallback releaseLatest failed error=$e');
return latest;
}
}
static bool _isVersionInfoEmpty(SCVersionManageLatestRes? version) {
if (version == null) {
return true;
}
return (version.id ?? '').trim().isEmpty &&
(version.platform ?? '').trim().isEmpty &&
(version.version ?? '').trim().isEmpty &&
version.forceUpdate == null &&
(version.appType ?? '').trim().isEmpty &&
version.review == null &&
version.patch == null &&
(version.updateDescribe ?? '').trim().isEmpty &&
(version.updateWorshipDescribe ?? '').trim().isEmpty &&
(version.downloadUrl ?? '').trim().isEmpty &&
version.buildVersion == null &&
version.createTime == null;
}
static int _compareVersion(String? latestVersion, String currentVersion) {
final latestParts = _versionParts(latestVersion);
final currentParts = _versionParts(currentVersion);
final maxLength =
latestParts.length > currentParts.length
? latestParts.length
: currentParts.length;
for (var i = 0; i < maxLength; i++) {
final latestPart = i < latestParts.length ? latestParts[i] : 0;
final currentPart = i < currentParts.length ? currentParts[i] : 0;
if (latestPart != currentPart) {
return latestPart.compareTo(currentPart);
}
}
return 0;
}
static List<int> _versionParts(String? version) {
final normalized = version?.trim() ?? "";
if (normalized.isEmpty) {
return const <int>[];
}
return normalized
.split(RegExp(r'[.+\-_]'))
.map(
(part) => int.tryParse(RegExp(r'^\d+').stringMatch(part) ?? '0') ?? 0,
)
.toList();
}
static int _currentRawBuild() {
return int.tryParse(SCGlobalConfig.build.trim()) ?? 0;
}
static int _currentComparableBuild() {
final rawBuild = _currentRawBuild();
if (!Platform.isAndroid || rawBuild < 1000) {
return rawBuild;
}
// Flutter split-per-abi APKs offset Android versionCode by ABI, e.g.
// build 132 becomes 2132 for arm64-v8a. The backend stores the base build.
final baseBuild = rawBuild % 1000;
return baseBuild > 0 ? baseBuild : rawBuild;
}
static String _currentBuildDebug() {
final rawBuild = _currentRawBuild();
final comparableBuild = _currentComparableBuild();
if (rawBuild == comparableBuild) {
return '$rawBuild';
}
return '$comparableBuild(raw=$rawBuild)';
}
static String _forceUpdateDecisionDebug(SCVersionManageLatestRes? latest) {
if (latest == null) {
return 'latest_null';
}
final latestBuild = latest.buildVersion?.toInt() ?? 0;
final currentBuild = _currentComparableBuild();
final currentBuildDebug = _currentBuildDebug();
final versionCompare = _compareVersion(
latest.version,
SCGlobalConfig.version,
);
if (latest.forceUpdate != true) {
return 'forceUpdate_not_true '
'forceUpdate=${latest.forceUpdate} '
'latestBuild=$latestBuild currentBuild=$currentBuildDebug '
'versionCompare=$versionCompare';
}
if (latestBuild > 0 && currentBuild > 0) {
return 'compare_build '
'latestBuild=$latestBuild currentBuild=$currentBuildDebug '
'versionCompare=$versionCompare';
}
return 'compare_version '
'latestVersion=${latest.version} '
'currentVersion=${SCGlobalConfig.version} '
'versionCompare=$versionCompare '
'latestBuild=$latestBuild currentBuild=$currentBuildDebug';
}
static String _versionInfoDebug(SCVersionManageLatestRes? version) {
if (version == null) {
return 'null';
}
return '{'
'id=${version.id}, '
'platform=${version.platform}, '
'version=${version.version}, '
'buildVersion=${version.buildVersion}, '
'forceUpdate=${version.forceUpdate}, '
'review=${version.review}, '
'patch=${version.patch}, '
'downloadUrl=${version.downloadUrl}'
'}';
}
static void _debugLog(String message) {
debugPrint('[SCVersionUtils] $message');
}
static void _showForceUpdateDialog(
BuildContext context,
SCVersionManageLatestRes latest,
) {
if (_forceUpdateDialogShowing || !context.mounted) {
return;
}
_forceUpdateDialogShowing = true;
unawaited(
showDialog<void>(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withValues(alpha: 0.62),
builder: (dialogContext) {
final version = (latest.version ?? "").trim();
final updateDescribe = (latest.updateDescribe ?? "").trim();
final message =
updateDescribe.isNotEmpty
? updateDescribe
: "A newer version is required to keep voice rooms stable "
"and secure.";
return PopScope(
canPop: false,
child: Dialog(
elevation: 0,
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 28),
child: SCForceUpdateDialog(
version: version,
message: message,
onUpdate: () {
unawaited(_openUpdateUrl(latest));
},
),
),
);
},
).whenComplete(() {
_forceUpdateDialogShowing = false;
}),
);
}
static Future<void> _openUpdateUrl(SCVersionManageLatestRes latest) async {
final url = _resolveDownloadUrl(latest);
if (url.isEmpty) {
return;
}
await SCUrlLauncherUtils.launchExternal(url);
}
static String _resolveDownloadUrl(SCVersionManageLatestRes latest) {
final downloadUrl = (latest.downloadUrl ?? "").trim();
if (downloadUrl.isNotEmpty) {
return downloadUrl;
}
return Platform.isIOS
? SCGlobalConfig.appDownloadUrlApple
: SCGlobalConfig.appDownloadUrlGoogle;
}
}
class SCForceUpdateDialog extends StatelessWidget {
const SCForceUpdateDialog({
super.key,
required this.version,
required this.message,
required this.onUpdate,
});
final String version;
final String message;
final VoidCallback onUpdate;
static const _asset = 'sc_images/general/sc_icon_app_update_bg.png';
static const _primary = Color(0xff18F2B1);
static const _primaryDark = Color(0xff10C894);
static const _titleColor = Color(0xff151823);
static const _bodyColor = Color(0xff626775);
@override
Widget build(BuildContext context) {
final constrainedTextScaler = MediaQuery.textScalerOf(
context,
).clamp(maxScaleFactor: 1.15);
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: constrainedTextScaler),
child: LayoutBuilder(
builder: (context, constraints) {
final width =
constraints.maxWidth.isFinite
? constraints.maxWidth.clamp(280.0, 340.0)
: 340.0;
return Center(
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(width: width),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 34,
offset: const Offset(0, 18),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 164,
width: double.infinity,
child: Image.asset(
_asset,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
frameBuilder: (
context,
child,
frame,
wasSynchronouslyLoaded,
) {
if (wasSynchronouslyLoaded || frame != null) {
return child;
}
return const _UpdateHeroFallback();
},
errorBuilder: (context, error, stackTrace) {
return const _UpdateHeroFallback();
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_VersionPill(version: version),
const SizedBox(height: 14),
const Text(
'Update Required',
textAlign: TextAlign.center,
style: TextStyle(
color: _titleColor,
fontSize: 22,
fontWeight: FontWeight.w800,
height: 1.16,
),
),
const SizedBox(height: 10),
Text(
message,
maxLines: 3,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: _bodyColor,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.45,
),
),
const SizedBox(height: 18),
const _UpdateNotice(),
const SizedBox(height: 22),
_UpdateButton(onPressed: onUpdate),
],
),
),
],
),
),
),
),
);
},
),
);
}
}
class _UpdateHeroFallback extends StatelessWidget {
const _UpdateHeroFallback();
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xff14D6A1), Color(0xff65F3CD)],
),
),
child: Stack(
children: [
Positioned(
left: 34,
top: 26,
child: Icon(
Icons.auto_awesome_rounded,
color: Colors.white.withValues(alpha: 0.72),
size: 18,
),
),
Positioned(
right: 44,
top: 36,
child: Icon(
Icons.auto_awesome_rounded,
color: Colors.white.withValues(alpha: 0.58),
size: 14,
),
),
Center(
child: Container(
width: 86,
height: 86,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.rocket_launch_rounded,
color: SCForceUpdateDialog._primaryDark,
size: 46,
),
),
),
],
),
);
}
}
class _VersionPill extends StatelessWidget {
const _VersionPill({required this.version});
final String version;
@override
Widget build(BuildContext context) {
final label =
version.isEmpty ? 'New version available' : 'Version $version';
return DecoratedBox(
decoration: BoxDecoration(
color: SCForceUpdateDialog._primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: SCForceUpdateDialog._primary.withValues(alpha: 0.28),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: SCForceUpdateDialog._primaryDark,
fontSize: 12,
fontWeight: FontWeight.w700,
height: 1.1,
),
),
),
);
}
}
class _UpdateNotice extends StatelessWidget {
const _UpdateNotice();
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xffF6F8FA),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: const Text(
'This Agora-based version has expired. Please update to the latest '
'version to continue using voice rooms normally.',
textAlign: TextAlign.center,
style: TextStyle(
color: SCForceUpdateDialog._titleColor,
fontSize: 13,
fontWeight: FontWeight.w600,
height: 1.36,
),
),
),
);
}
}
class _UpdateButton extends StatelessWidget {
const _UpdateButton({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Ink(
height: 50,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [SCForceUpdateDialog._primary, Color(0xff76FFD8)],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: SCForceUpdateDialog._primary.withValues(alpha: 0.34),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(16),
child: const Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Update Now',
style: TextStyle(
color: Color(0xff10131A),
fontSize: 16,
fontWeight: FontWeight.w800,
height: 1,
),
),
SizedBox(width: 8),
Icon(
Icons.arrow_forward_rounded,
color: Color(0xff10131A),
size: 20,
),
],
),
),
),
),
);
}
}

View File

@ -48,8 +48,6 @@ class SVGAHeadwearWidget extends StatefulWidget {
class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget> class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
SVGAAnimationController? _animationController; SVGAAnimationController? _animationController;
MovieEntity? _retainedMovieEntity;
String? _retainedResource;
bool _isLoading = true; bool _isLoading = true;
bool _isNetworkResource = false; bool _isNetworkResource = false;
bool _hasError = false; bool _hasError = false;
@ -84,53 +82,41 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
}); });
try { try {
//
MovieEntity? videoItem;
if (widget.useCache) { if (widget.useCache) {
final videoItem = videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource];
_retainedResource == widget.resource && _retainedMovieEntity != null }
? _retainedMovieEntity!
: await SCGiftVapSvgaManager().acquireSvgaEntity(
widget.resource,
);
final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity);
if (!mounted) {
if (acquiredNewEntity) {
SCGiftVapSvgaManager().releaseSvgaEntity(videoItem);
}
return;
}
setState(() { //
_releaseRetainedMovieEntity(except: videoItem); if (videoItem == null) {
_retainedMovieEntity = videoItem; videoItem =
_retainedResource = widget.resource;
_animationController?.videoItem = videoItem;
_isLoading = false;
});
} else {
final videoItem =
_isNetworkResource _isNetworkResource
? await SVGAParser.shared.decodeFromURL(widget.resource) ? await SVGAParser.shared.decodeFromURL(widget.resource)
: await SVGAParser.shared.decodeFromAssets(widget.resource); : await SVGAParser.shared.decodeFromAssets(widget.resource);
if (!mounted) { videoItem.autorelease = false;
videoItem.dispose(); //
return; if (widget.useCache) {
SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem;
} }
}
if (mounted) {
setState(() { setState(() {
_animationController?.videoItem = videoItem; _animationController?.videoItem = videoItem;
_isLoading = false; _isLoading = false;
}); });
}
// //
if (widget.loops == 0) { if (widget.loops == 0) {
_animationController?.repeat(); _animationController?.repeat();
} else { } else {
_animationController?.repeat(count: widget.loops); _animationController?.repeat(count: widget.loops);
} }
if (widget.onFinishLoading != null) { if (widget.onFinishLoading != null) {
widget.onFinishLoading!(); widget.onFinishLoading!();
}
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
@ -171,16 +157,14 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
_animationController?.stop(); _animationController?.stop();
if (widget.clearsAfterStop) { if (widget.clearsAfterStop) {
_animationController?.videoItem = null; _animationController?.videoItem = null;
_releaseRetainedMovieEntity();
} }
} }
// //
void reload() { void reload() {
if (widget.useCache) { if (widget.useCache) {
_animationController?.videoItem = null; //
_releaseRetainedMovieEntity(); SCGiftVapSvgaManager().videoItemCache.remove(widget.resource);
SCGiftVapSvgaManager().evictSvgaEntity(widget.resource);
} }
_loadAnimation(); _loadAnimation();
} }
@ -189,20 +173,9 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
void dispose() { void dispose() {
stop(); stop();
_animationController?.dispose(); _animationController?.dispose();
_releaseRetainedMovieEntity();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedResource = null;
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading && !widget.autoPlay) { if (_isLoading && !widget.autoPlay) {

View File

@ -270,8 +270,6 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
bool _isLoading = false; bool _isLoading = false;
bool _hasError = false; bool _hasError = false;
String? _currentResource; String? _currentResource;
MovieEntity? _retainedMovieEntity;
String? _retainedResource;
// SVGAImage组件的key便 // SVGAImage组件的key便
GlobalKey _svgaKey = GlobalKey(); GlobalKey _svgaKey = GlobalKey();
@ -353,79 +351,45 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
_hasError = false; _hasError = false;
}); });
MovieEntity? acquiredMovieEntity;
try { try {
final isNetworkResource = resource.startsWith('http');
MovieEntity? videoItem;
if (widget.useCache) { if (widget.useCache) {
final videoItem = videoItem = SCGiftVapSvgaManager().videoItemCache[resource];
_retainedResource == resource && _retainedMovieEntity != null }
? _retainedMovieEntity!
: await SCGiftVapSvgaManager().acquireSvgaEntity(resource);
final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity);
if (acquiredNewEntity) {
acquiredMovieEntity = videoItem;
}
if (!mounted) {
if (acquiredNewEntity) {
SCGiftVapSvgaManager().releaseSvgaEntity(videoItem);
acquiredMovieEntity = null;
}
return;
}
// if (videoItem == null) {
await _applyDynamicData(videoItem, dynamicData); videoItem =
if (!mounted) {
if (acquiredNewEntity) {
SCGiftVapSvgaManager().releaseSvgaEntity(videoItem);
acquiredMovieEntity = null;
}
return;
}
setState(() {
_releaseRetainedMovieEntity(except: videoItem);
_retainedMovieEntity = videoItem;
_retainedResource = resource;
_animationController?.videoItem = videoItem;
_isLoading = false;
});
acquiredMovieEntity = null;
} else {
final isNetworkResource = resource.startsWith('http');
final videoItem =
isNetworkResource isNetworkResource
? await SVGAParser.shared.decodeFromURL(resource) ? await SVGAParser.shared.decodeFromURL(resource)
: await SVGAParser.shared.decodeFromAssets(resource); : await SVGAParser.shared.decodeFromAssets(resource);
if (!mounted) { videoItem.autorelease = false;
videoItem.dispose();
return;
}
if (widget.useCache) {
SCGiftVapSvgaManager().videoItemCache[resource] = videoItem;
}
}
if (mounted) {
// //
await _applyDynamicData(videoItem, dynamicData); await _applyDynamicData(videoItem, dynamicData);
if (!mounted) {
videoItem.dispose();
return;
}
setState(() { setState(() {
_animationController?.videoItem = videoItem; _animationController?.videoItem = videoItem;
_isLoading = false; _isLoading = false;
}); });
}
// //
_startAnimation(); _startAnimation();
if (widget.onFinishLoading != null) { if (widget.onFinishLoading != null) {
widget.onFinishLoading!(); widget.onFinishLoading!();
}
} }
} catch (e) { } catch (e) {
SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity);
if (mounted) { if (mounted) {
setState(() { setState(() {
_animationController?.videoItem = null;
_releaseRetainedMovieEntity();
_isLoading = false; _isLoading = false;
_hasError = true; _hasError = true;
}); });
@ -579,7 +543,6 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
_animationController?.stop(); _animationController?.stop();
if (widget.clearsAfterStop) { if (widget.clearsAfterStop) {
_animationController?.videoItem = null; _animationController?.videoItem = null;
_releaseRetainedMovieEntity();
setState(() { setState(() {
_currentResource = null; _currentResource = null;
}); });
@ -590,9 +553,7 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
void reload() { void reload() {
if (_currentResource != null) { if (_currentResource != null) {
if (widget.useCache) { if (widget.useCache) {
_animationController?.videoItem = null; SCGiftVapSvgaManager().videoItemCache.remove(_currentResource);
_releaseRetainedMovieEntity();
SCGiftVapSvgaManager().evictSvgaEntity(_currentResource!);
} }
_loadAnimation( _loadAnimation(
@ -607,22 +568,11 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
void dispose() { void dispose() {
stop(); stop();
_animationController?.dispose(); _animationController?.dispose();
_releaseRetainedMovieEntity();
// //
_queueManager.clearCurrentPlayer(); _queueManager.clearCurrentPlayer();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedResource = null;
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// //

View File

@ -74,11 +74,16 @@ class _ExitMinRoomPageState extends State<ExitMinRoomPage> {
final giftAnimationManager = final giftAnimationManager =
context.read<GiftAnimationManager>(); context.read<GiftAnimationManager>();
final rtmProvider = context.read<RtmProvider>(); final rtmProvider = context.read<RtmProvider>();
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id
?.trim() ??
'';
rtcProvider.setVoiceRoomRouteVisible(false);
rtcProvider.setRoomVisualEffectsEnabled(false); rtcProvider.setRoomVisualEffectsEnabled(false);
rtmProvider.msgFloatingGiftListener = null; rtmProvider.msgFloatingGiftListener = null;
rtmProvider.msgLuckyGiftRewardTickerListener = null; rtmProvider.msgLuckyGiftRewardTickerListener = null;
RoomEntranceHelper.clearQueue(); RoomEntranceHelper.clearQueue();
OverlayManager().removeRoom(); OverlayManager().removeRoom(roomId: currentRoomId);
SCRoomEffectScheduler().clearDeferredTasks( SCRoomEffectScheduler().clearDeferredTasks(
reason: 'room_minimize', reason: 'room_minimize',
); );

View File

@ -22,6 +22,8 @@ class RoomPlayWidget extends StatefulWidget {
} }
class _RoomPlayWidgetState extends State<RoomPlayWidget> { class _RoomPlayWidgetState extends State<RoomPlayWidget> {
static const double _rightActionDownShift = 24;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox.expand( return SizedBox.expand(
@ -29,17 +31,17 @@ class _RoomPlayWidgetState extends State<RoomPlayWidget> {
children: [ children: [
PositionedDirectional( PositionedDirectional(
end: 15.w, end: 15.w,
bottom: 150.w, bottom: (150 - _rightActionDownShift).w,
child: const RoomMusicFloatingEntry(), child: const RoomMusicFloatingEntry(),
), ),
PositionedDirectional( PositionedDirectional(
end: 15.w, end: 15.w,
bottom: 200.w, bottom: (200 - _rightActionDownShift).w,
child: const _RoomRedPacketFloatingEntry(), child: const _RoomRedPacketFloatingEntry(),
), ),
PositionedDirectional( PositionedDirectional(
end: 15.w, end: 15.w,
bottom: 100.w, bottom: (100 - _rightActionDownShift).w,
child: const RoomGameEntryButton(), child: const RoomGameEntryButton(),
), ),
PositionedDirectional( PositionedDirectional(

View File

@ -52,8 +52,6 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller; late final SVGAAnimationController _controller;
String? _loadedResource; String? _loadedResource;
MovieEntity? _retainedMovieEntity;
String? _retainedResource;
bool _hasError = false; bool _hasError = false;
@override @override
@ -80,10 +78,9 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
final resource = widget.resource.trim(); final resource = widget.resource.trim();
if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) { if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) {
setState(() { setState(() {
_controller.videoItem = null;
_releaseRetainedMovieEntity();
_hasError = true; _hasError = true;
_loadedResource = null; _loadedResource = null;
_controller.videoItem = null;
}); });
return; return;
} }
@ -92,53 +89,37 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
_hasError = false; _hasError = false;
}); });
MovieEntity? acquiredMovieEntity;
try { try {
final movieEntity = final cache = SCGiftVapSvgaManager().videoItemCache;
_retainedResource == resource && _retainedMovieEntity != null var movieEntity = cache[resource];
? _retainedMovieEntity! if (movieEntity == null) {
: await SCGiftVapSvgaManager().acquireSvgaEntity(resource); movieEntity =
final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity); resource.startsWith("http")
if (acquiredNewEntity) { ? await SVGAParser.shared.decodeFromURL(resource)
acquiredMovieEntity = movieEntity; : await SVGAParser.shared.decodeFromAssets(resource);
} movieEntity.autorelease = false;
if (!mounted || widget.resource.trim() != resource) { cache[resource] = movieEntity;
if (acquiredNewEntity) {
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
acquiredMovieEntity = null;
}
return;
} }
if (widget.movieConfigurer != null) { if (widget.movieConfigurer != null) {
movieEntity.dynamicItem.reset(); movieEntity.dynamicItem.reset();
await widget.movieConfigurer!(movieEntity); await widget.movieConfigurer!(movieEntity);
} }
if (!mounted || widget.resource.trim() != resource) { if (!mounted || widget.resource.trim() != resource) {
if (acquiredNewEntity) {
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
acquiredMovieEntity = null;
}
return; return;
} }
setState(() { setState(() {
_releaseRetainedMovieEntity(except: movieEntity);
_retainedMovieEntity = movieEntity;
_retainedResource = resource;
_loadedResource = resource; _loadedResource = resource;
_controller.videoItem = movieEntity; _controller.videoItem = movieEntity;
}); });
acquiredMovieEntity = null;
_syncPlayback(restartIfActive: true); _syncPlayback(restartIfActive: true);
} catch (error) { } catch (error) {
SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity);
if (!mounted || widget.resource.trim() != resource) { if (!mounted || widget.resource.trim() != resource) {
return; return;
} }
setState(() { setState(() {
_controller.videoItem = null;
_releaseRetainedMovieEntity();
_hasError = true; _hasError = true;
_loadedResource = null; _loadedResource = null;
_controller.videoItem = null;
}); });
} }
} }
@ -164,20 +145,9 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
_releaseRetainedMovieEntity();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedResource = null;
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoItem = _controller.videoItem; final videoItem = _controller.videoItem;

View File

@ -8,11 +8,9 @@ typedef SCSvgaMovieConfigurer =
FutureOr<void> Function(MovieEntity movieEntity); FutureOr<void> Function(MovieEntity movieEntity);
class SCSvgaAssetWidget extends StatefulWidget { class SCSvgaAssetWidget extends StatefulWidget {
static const int _maxCacheEntries = 8;
static final Map<String, MovieEntity> _cache = <String, MovieEntity>{}; static final Map<String, MovieEntity> _cache = <String, MovieEntity>{};
static final Map<String, Future<MovieEntity>> _loadingTasks = static final Map<String, Future<MovieEntity>> _loadingTasks =
<String, Future<MovieEntity>>{}; <String, Future<MovieEntity>>{};
static final Map<MovieEntity, int> _activeUseCounts = <MovieEntity, int>{};
const SCSvgaAssetWidget({ const SCSvgaAssetWidget({
super.key, super.key,
@ -51,26 +49,8 @@ class SCSvgaAssetWidget extends StatefulWidget {
return _obtainMovieEntity(assetPath); return _obtainMovieEntity(assetPath);
} }
static void clearMemoryCache({bool includeActive = false}) {
final entries = _cache.entries.toList(growable: false);
for (final entry in entries) {
final entity = entry.value;
if (!includeActive && _isActive(entity)) {
continue;
}
_cache.remove(entry.key);
_disposeMovieEntity(entity);
}
}
static Future<MovieEntity> _acquireMovieEntity(String assetPath) async {
final entity = await _obtainMovieEntity(assetPath);
_retainMovieEntity(entity);
return entity;
}
static Future<MovieEntity> _obtainMovieEntity(String assetPath) async { static Future<MovieEntity> _obtainMovieEntity(String assetPath) async {
final cached = _touchCachedMovieEntity(assetPath); final cached = _cache[assetPath];
if (cached != null) { if (cached != null) {
return cached; return cached;
} }
@ -91,7 +71,7 @@ class SCSvgaAssetWidget extends StatefulWidget {
); );
} }
entity.autorelease = false; entity.autorelease = false;
_cacheMovieEntity(assetPath, entity); _cache[assetPath] = entity;
return entity; return entity;
}(); }();
@ -103,77 +83,6 @@ class SCSvgaAssetWidget extends StatefulWidget {
} }
} }
static MovieEntity? _touchCachedMovieEntity(String assetPath) {
final cached = _cache.remove(assetPath);
if (cached != null) {
_cache[assetPath] = cached;
}
return cached;
}
static void _cacheMovieEntity(String assetPath, MovieEntity entity) {
final previous = _cache.remove(assetPath);
if (previous != null && !identical(previous, entity)) {
_disposeMovieEntityIfInactive(previous);
}
_cache[assetPath] = entity;
_trimCache();
}
static void _trimCache() {
while (_cache.length > _maxCacheEntries) {
final removableEntry = _cache.entries.firstWhere(
(entry) => !_isActive(entry.value),
orElse: () => _cache.entries.first,
);
if (_isActive(removableEntry.value)) {
_cache.remove(removableEntry.key);
_cache[removableEntry.key] = removableEntry.value;
break;
}
_cache.remove(removableEntry.key);
_disposeMovieEntity(removableEntry.value);
}
}
static void _retainMovieEntity(MovieEntity entity) {
_activeUseCounts[entity] = (_activeUseCounts[entity] ?? 0) + 1;
}
static void _releaseMovieEntity(MovieEntity? entity) {
if (entity == null) {
return;
}
final count = _activeUseCounts[entity] ?? 0;
if (count <= 1) {
_activeUseCounts.remove(entity);
if (!_cache.containsValue(entity)) {
_disposeMovieEntity(entity);
} else {
_trimCache();
}
return;
}
_activeUseCounts[entity] = count - 1;
}
static bool _isActive(MovieEntity entity) =>
(_activeUseCounts[entity] ?? 0) > 0;
static void _disposeMovieEntityIfInactive(MovieEntity entity) {
if (_isActive(entity)) {
return;
}
_disposeMovieEntity(entity);
}
static void _disposeMovieEntity(MovieEntity entity) {
try {
_activeUseCounts.remove(entity);
entity.dispose();
} catch (_) {}
}
@override @override
State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState(); State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState();
} }
@ -182,8 +91,6 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller; late final SVGAAnimationController _controller;
String? _loadedAssetPath; String? _loadedAssetPath;
MovieEntity? _retainedMovieEntity;
String? _retainedAssetPath;
bool _hasError = false; bool _hasError = false;
@override @override
@ -224,44 +131,25 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
setState(() { setState(() {
_hasError = false; _hasError = false;
}); });
MovieEntity? acquiredMovieEntity;
try { try {
final movieEntity = final movieEntity = await SCSvgaAssetWidget._obtainMovieEntity(assetPath);
_retainedAssetPath == assetPath && _retainedMovieEntity != null
? _retainedMovieEntity!
: await SCSvgaAssetWidget._acquireMovieEntity(assetPath);
final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity);
if (acquiredNewEntity) {
acquiredMovieEntity = movieEntity;
}
if (widget.movieConfigurer != null) { if (widget.movieConfigurer != null) {
movieEntity.dynamicItem.reset(); movieEntity.dynamicItem.reset();
await widget.movieConfigurer!(movieEntity); await widget.movieConfigurer!(movieEntity);
} }
if (!mounted || widget.assetPath != assetPath) { if (!mounted || widget.assetPath != assetPath) {
if (acquiredNewEntity) {
SCSvgaAssetWidget._releaseMovieEntity(movieEntity);
acquiredMovieEntity = null;
}
return; return;
} }
setState(() { setState(() {
_releaseRetainedMovieEntity(except: movieEntity);
_retainedMovieEntity = movieEntity;
_retainedAssetPath = assetPath;
_loadedAssetPath = assetPath; _loadedAssetPath = assetPath;
_controller.videoItem = movieEntity; _controller.videoItem = movieEntity;
}); });
acquiredMovieEntity = null;
_syncPlayback(restartIfActive: true); _syncPlayback(restartIfActive: true);
} catch (error) { } catch (error) {
SCSvgaAssetWidget._releaseMovieEntity(acquiredMovieEntity);
if (!mounted || widget.assetPath != assetPath) { if (!mounted || widget.assetPath != assetPath) {
return; return;
} }
setState(() { setState(() {
_controller.videoItem = null;
_releaseRetainedMovieEntity();
_hasError = true; _hasError = true;
}); });
} }
@ -297,20 +185,9 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
void dispose() { void dispose() {
_controller.removeStatusListener(_handleAnimationStatusChanged); _controller.removeStatusListener(_handleAnimationStatusChanged);
_controller.dispose(); _controller.dispose();
_releaseRetainedMovieEntity();
super.dispose(); super.dispose();
} }
void _releaseRetainedMovieEntity({MovieEntity? except}) {
final entity = _retainedMovieEntity;
if (entity == null || identical(entity, except)) {
return;
}
_retainedMovieEntity = null;
_retainedAssetPath = null;
SCSvgaAssetWidget._releaseMovieEntity(entity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_hasError) { if (_hasError) {

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.3.1+12 version: 1.3.1+131
environment: environment: