Compare commits
6 Commits
85e6d88472
...
f2e9ac3c60
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e9ac3c60 | ||
|
|
92bbbcb0eb | ||
|
|
829914c58c | ||
|
|
39c5fbf208 | ||
|
|
2a1b63de24 | ||
|
|
fec672dfb6 |
@ -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 = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -511,7 +511,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.0;
|
MARKETING_VERSION = 1.3.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -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 = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -702,7 +702,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.0;
|
MARKETING_VERSION = 1.3.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -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 = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -731,7 +731,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.0;
|
MARKETING_VERSION = 1.3.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import 'package:fluro/fluro.dart' as fluro;
|
import 'package:fluro/fluro.dart' as fluro;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/app/routes/sc_router_init.dart';
|
import 'package:yumi/app/routes/sc_router_init.dart';
|
||||||
import 'package:yumi/modules/country/country_route.dart';
|
import 'package:yumi/modules/country/country_route.dart';
|
||||||
import 'package:yumi/modules/index/main_route.dart';
|
import 'package:yumi/modules/index/main_route.dart';
|
||||||
import 'package:yumi/modules/chat/chat_route.dart';
|
import 'package:yumi/modules/chat/chat_route.dart';
|
||||||
import 'package:yumi/modules/store/store_route.dart';
|
import 'package:yumi/modules/store/store_route.dart';
|
||||||
import 'package:yumi/modules/index/index_page.dart';
|
import 'package:yumi/modules/index/index_page.dart';
|
||||||
|
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||||
import 'package:yumi/modules/user/settings/settings_route.dart';
|
import 'package:yumi/modules/user/settings/settings_route.dart';
|
||||||
import 'package:yumi/modules/user/task/task_route.dart';
|
import 'package:yumi/modules/user/task/task_route.dart';
|
||||||
import 'package:yumi/modules/user/vip/vip_route.dart';
|
import 'package:yumi/modules/user/vip/vip_route.dart';
|
||||||
@ -32,7 +34,7 @@ class SCRoutes {
|
|||||||
handler: fluro.Handler(
|
handler: fluro.Handler(
|
||||||
handlerFunc:
|
handlerFunc:
|
||||||
(BuildContext? context, Map<String, List<String>> params) =>
|
(BuildContext? context, Map<String, List<String>> params) =>
|
||||||
SCIndexPage(),
|
_buildHomePage(context),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -57,6 +59,13 @@ class SCRoutes {
|
|||||||
routerProvider.initRouter(router);
|
routerProvider.initRouter(router);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Widget _buildHomePage(BuildContext? context) {
|
||||||
|
return ChangeNotifierProvider<SocialChatRoomManager>.value(
|
||||||
|
value: resolveSocialChatRoomManager(context),
|
||||||
|
child: const SCIndexPage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PS:路由使用方法
|
// PS:路由使用方法
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import 'app/routes/sc_routes.dart';
|
|||||||
import 'app/routes/sc_lk_application.dart';
|
import 'app/routes/sc_lk_application.dart';
|
||||||
import 'shared/tools/sc_deep_link_handler.dart';
|
import 'shared/tools/sc_deep_link_handler.dart';
|
||||||
import 'shared/tools/sc_deviceId_utils.dart';
|
import 'shared/tools/sc_deviceId_utils.dart';
|
||||||
|
import 'shared/tools/sc_gift_vap_svga_manager.dart';
|
||||||
import 'shared/data_sources/sources/local/user_manager.dart';
|
import 'shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'modules/splash/splash_page.dart';
|
import 'modules/splash/splash_page.dart';
|
||||||
import 'services/general/sc_app_general_manager.dart';
|
import 'services/general/sc_app_general_manager.dart';
|
||||||
@ -45,6 +46,7 @@ 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;
|
||||||
|
|
||||||
@ -105,6 +107,16 @@ void _configureImageCache() {
|
|||||||
imageCache.maximumSize = SCGlobalConfig.recommendedImageCacheEntries;
|
imageCache.maximumSize = SCGlobalConfig.recommendedImageCacheEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _releaseVisualMemoryCaches({bool clearLiveImages = false}) {
|
||||||
|
SCGiftVapSvgaManager().clearMemoryCache();
|
||||||
|
SCSvgaAssetWidget.clearMemoryCache();
|
||||||
|
final imageCache = PaintingBinding.instance.imageCache;
|
||||||
|
imageCache.clear();
|
||||||
|
if (clearLiveImages) {
|
||||||
|
imageCache.clearLiveImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _installErrorHandlers() {
|
void _installErrorHandlers() {
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
FlutterError.presentError(details);
|
FlutterError.presentError(details);
|
||||||
@ -229,9 +241,8 @@ class RootAppWithProviders extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// 房间与社交功能Provider - 懒加载
|
// 房间与社交功能Provider - 懒加载
|
||||||
ChangeNotifierProvider<SocialChatRoomManager>(
|
ChangeNotifierProvider<SocialChatRoomManager>.value(
|
||||||
lazy: true,
|
value: socialChatRoomManager,
|
||||||
create: (context) => SocialChatRoomManager(),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 礼物与动画系统Provider - 懒加载
|
// 礼物与动画系统Provider - 懒加载
|
||||||
@ -303,6 +314,14 @@ class _YumiApplicationState extends State<YumiApplication>
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
super.didChangeAppLifecycleState(state);
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.paused ||
|
||||||
|
state == AppLifecycleState.inactive ||
|
||||||
|
state == AppLifecycleState.hidden) {
|
||||||
|
SCGiftVapSvgaManager().stopPlayback();
|
||||||
|
_releaseVisualMemoryCaches(
|
||||||
|
clearLiveImages: state == AppLifecycleState.paused,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (state == AppLifecycleState.detached) {
|
if (state == AppLifecycleState.detached) {
|
||||||
unawaited(
|
unawaited(
|
||||||
context.read<RtcProvider>().releaseRtcEngineForAppTermination(),
|
context.read<RtcProvider>().releaseRtcEngineForAppTermination(),
|
||||||
@ -310,6 +329,13 @@ class _YumiApplicationState extends State<YumiApplication>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didHaveMemoryPressure() {
|
||||||
|
super.didHaveMemoryPressure();
|
||||||
|
SCGiftVapSvgaManager().stopPlayback();
|
||||||
|
_releaseVisualMemoryCaches(clearLiveImages: true);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initLink() async {
|
Future<void> _initLink() async {
|
||||||
// 初始化,并传递一个回调函数用于处理链接
|
// 初始化,并传递一个回调函数用于处理链接
|
||||||
await _deepLinkHandler.initDeepLinks(
|
await _deepLinkHandler.initDeepLinks(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:dio/dio.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';
|
||||||
@ -364,7 +365,7 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
///登录
|
///登录
|
||||||
_login() async {
|
Future<void> _login() async {
|
||||||
String account = accountController.text;
|
String account = accountController.text;
|
||||||
String pass = passController.text;
|
String pass = passController.text;
|
||||||
if (account.isEmpty || pass.isEmpty) {
|
if (account.isEmpty || pass.isEmpty) {
|
||||||
@ -394,15 +395,47 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
|
|||||||
}
|
}
|
||||||
AccountStorage().setCurrentUser(user);
|
AccountStorage().setCurrentUser(user);
|
||||||
SCLoadingManager.hide();
|
SCLoadingManager.hide();
|
||||||
DataPersistence.setString("Login_Account", account);
|
try {
|
||||||
DataPersistence.setString("Login_Pwd", pass);
|
await Future.wait([
|
||||||
|
DataPersistence.setString("Login_Account", account),
|
||||||
|
DataPersistence.setString("Login_Pwd", pass),
|
||||||
|
]);
|
||||||
|
} catch (_) {}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true);
|
SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
SCLoadingManager.hide();
|
SCLoadingManager.hide();
|
||||||
// 可以添加错误处理逻辑
|
final message = _loginErrorMessage(e);
|
||||||
rethrow;
|
if (message.isNotEmpty) {
|
||||||
|
SCTts.show(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _loginErrorMessage(Object error) {
|
||||||
|
if (error is DioException) {
|
||||||
|
final responseData = error.response?.data;
|
||||||
|
if (responseData is Map) {
|
||||||
|
final serverMessage = responseData["errorMsg"]?.toString().trim();
|
||||||
|
if (serverMessage != null && serverMessage.isNotEmpty) {
|
||||||
|
return serverMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final dioError = error.error?.toString().trim();
|
||||||
|
if (dioError != null && dioError.isNotEmpty) {
|
||||||
|
return dioError.replaceFirst("Exception: ", "");
|
||||||
|
}
|
||||||
|
final message = error.message?.trim();
|
||||||
|
if (message != null && message.isNotEmpty) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = error.toString().replaceFirst("Exception: ", "").trim();
|
||||||
|
if (message.isNotEmpty) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return "Login failed. Please try again.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -640,7 +640,9 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
|
|||||||
authType = SCAuthType.GOOGLE.name;
|
authType = SCAuthType.GOOGLE.name;
|
||||||
idToken = googleUid;
|
idToken = googleUid;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (_) {
|
||||||
|
// Fallback auth lookup is best effort; the form will ask to sign in again below.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (authType.isEmpty || idToken.isEmpty) {
|
if (authType.isEmpty || idToken.isEmpty) {
|
||||||
SCLoadingManager.hide();
|
SCLoadingManager.hide();
|
||||||
|
|||||||
@ -563,7 +563,8 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await SCEntryPopupCoordinator.openFirstPartyRoomRandomGame(context);
|
await SCEntryPopupCoordinator.openFirstPartyRoomRandomGame(context);
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
|
// The entry popup is optional; failure should not block the home page.
|
||||||
} finally {
|
} finally {
|
||||||
_isOpeningFirstRegisterRoomGame = false;
|
_isOpeningFirstRegisterRoomGame = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class _AllChatPageState extends State<AllChatPage> {
|
|||||||
bool _isDisposed = false;
|
bool _isDisposed = false;
|
||||||
final List<Msg> _msgList = [];
|
final List<Msg> _msgList = [];
|
||||||
late RtmProvider provider;
|
late RtmProvider provider;
|
||||||
|
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -28,7 +29,7 @@ class _AllChatPageState extends State<AllChatPage> {
|
|||||||
_controller.addListener(_scrollListener);
|
_controller.addListener(_scrollListener);
|
||||||
provider = Provider.of<RtmProvider>(context, listen: false);
|
provider = Provider.of<RtmProvider>(context, listen: false);
|
||||||
final msgList = provider.roomAllMsgList;
|
final msgList = provider.roomAllMsgList;
|
||||||
provider.msgAllListener = _onNewMsg;
|
provider.msgAllListener = _newMsgListener;
|
||||||
_msgList.addAll(msgList);
|
_msgList.addAll(msgList);
|
||||||
// 使用安全的方式初始化滚动位置
|
// 使用安全的方式初始化滚动位置
|
||||||
_initScrollPosition();
|
_initScrollPosition();
|
||||||
@ -81,7 +82,9 @@ class _AllChatPageState extends State<AllChatPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
|
if (identical(provider.msgAllListener, _newMsgListener)) {
|
||||||
provider.msgAllListener = null;
|
provider.msgAllListener = null;
|
||||||
|
}
|
||||||
_controller.removeListener(_scrollListener);
|
_controller.removeListener(_scrollListener);
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
bool _isDisposed = false;
|
bool _isDisposed = false;
|
||||||
List<Msg> _msgList = [];
|
List<Msg> _msgList = [];
|
||||||
late RtmProvider provider;
|
late RtmProvider provider;
|
||||||
|
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -28,7 +29,7 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
_controller.addListener(_scrollListener);
|
_controller.addListener(_scrollListener);
|
||||||
provider = Provider.of<RtmProvider>(context, listen: false);
|
provider = Provider.of<RtmProvider>(context, listen: false);
|
||||||
var msgList = provider.roomChatMsgList;
|
var msgList = provider.roomChatMsgList;
|
||||||
provider.msgChatListener = _onNewMsg;
|
provider.msgChatListener = _newMsgListener;
|
||||||
_msgList.addAll(msgList ??= []);
|
_msgList.addAll(msgList ??= []);
|
||||||
// 使用安全的方式初始化滚动位置
|
// 使用安全的方式初始化滚动位置
|
||||||
_initScrollPosition();
|
_initScrollPosition();
|
||||||
@ -80,7 +81,9 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
|
if (identical(provider.msgChatListener, _newMsgListener)) {
|
||||||
provider.msgChatListener = null;
|
provider.msgChatListener = null;
|
||||||
|
}
|
||||||
_controller.removeListener(_scrollListener);
|
_controller.removeListener(_scrollListener);
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
|
|||||||
bool _isDisposed = false;
|
bool _isDisposed = false;
|
||||||
final List<Msg> _msgList = [];
|
final List<Msg> _msgList = [];
|
||||||
late RtmProvider provider;
|
late RtmProvider provider;
|
||||||
|
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -28,7 +29,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
|
|||||||
_controller.addListener(_scrollListener);
|
_controller.addListener(_scrollListener);
|
||||||
provider = Provider.of<RtmProvider>(context, listen: false);
|
provider = Provider.of<RtmProvider>(context, listen: false);
|
||||||
final msgList = provider.roomGiftMsgList;
|
final msgList = provider.roomGiftMsgList;
|
||||||
provider.msgGiftListener = _onNewMsg;
|
provider.msgGiftListener = _newMsgListener;
|
||||||
_msgList.addAll(msgList);
|
_msgList.addAll(msgList);
|
||||||
// 使用安全的方式初始化滚动位置
|
// 使用安全的方式初始化滚动位置
|
||||||
_initScrollPosition();
|
_initScrollPosition();
|
||||||
@ -81,7 +82,9 @@ class _GiftChatPageState extends State<GiftChatPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
|
if (identical(provider.msgGiftListener, _newMsgListener)) {
|
||||||
provider.msgGiftListener = null;
|
provider.msgGiftListener = null;
|
||||||
|
}
|
||||||
_controller.removeListener(_scrollListener);
|
_controller.removeListener(_scrollListener);
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@ -52,6 +52,7 @@ class VoiceRoomPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _VoiceRoomPageState extends State<VoiceRoomPage>
|
class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
|
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);
|
||||||
static const int _maxLuckyGiftTrackedAnimations = 5;
|
static const int _maxLuckyGiftTrackedAnimations = 5;
|
||||||
@ -65,7 +66,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
static const Duration _giftVisualBatchDedupeWindow = Duration(seconds: 4);
|
static const Duration _giftVisualBatchDedupeWindow = Duration(seconds: 4);
|
||||||
|
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
|
|
||||||
late StreamSubscription _subscription;
|
late StreamSubscription _subscription;
|
||||||
final RoomGiftSeatFlightController _giftSeatFlightController =
|
final RoomGiftSeatFlightController _giftSeatFlightController =
|
||||||
RoomGiftSeatFlightController();
|
RoomGiftSeatFlightController();
|
||||||
@ -80,7 +80,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tabController = TabController(length: _pages.length, vsync: this);
|
_tabController = TabController(length: _chatTabCount, vsync: this);
|
||||||
_enableRoomVisualEffects();
|
_enableRoomVisualEffects();
|
||||||
_tabController.addListener(_handleTabChange);
|
_tabController.addListener(_handleTabChange);
|
||||||
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||||
@ -277,6 +277,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final chatRoomKey = _chatRoomKey(context);
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: false,
|
canPop: false,
|
||||||
onPopInvokedWithResult: (bool didPop, Object? result) {
|
onPopInvokedWithResult: (bool didPop, Object? result) {
|
||||||
@ -334,7 +335,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_buildChatView(),
|
_buildChatView(chatRoomKey),
|
||||||
const RoomBottomWidget(showGiftComboButton: false),
|
const RoomBottomWidget(showGiftComboButton: false),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -373,7 +374,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
///消息
|
///消息
|
||||||
Widget _buildChatView() {
|
Widget _buildChatView(String chatRoomKey) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
alignment: AlignmentDirectional.bottomEnd,
|
||||||
@ -418,7 +419,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
SCGlobalConfig.businessLogicStrategy
|
SCGlobalConfig.businessLogicStrategy
|
||||||
.getVoiceRoomTabDividerColor(),
|
.getVoiceRoomTabDividerColor(),
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
tabs: List<Widget>.generate(_pages.length, _buildImageTab),
|
tabs: List<Widget>.generate(_chatTabCount, _buildImageTab),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -436,7 +437,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
removeTop: true,
|
removeTop: true,
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: _pages,
|
children: _buildChatPages(chatRoomKey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -449,6 +450,28 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _chatRoomKey(BuildContext context) {
|
||||||
|
final roomProfile =
|
||||||
|
context.read<RtcProvider>().currenRoom?.roomProfile?.roomProfile;
|
||||||
|
final roomId = roomProfile?.id?.trim() ?? "";
|
||||||
|
final groupId = roomProfile?.roomAccount?.trim() ?? "";
|
||||||
|
return "$roomId|$groupId";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildChatPages(String chatRoomKey) {
|
||||||
|
return <Widget>[
|
||||||
|
KeyedSubtree(
|
||||||
|
key: ValueKey("all_chat_$chatRoomKey"),
|
||||||
|
child: const AllChatPage(),
|
||||||
|
),
|
||||||
|
KeyedSubtree(key: ValueKey("chat_$chatRoomKey"), child: ChatPage()),
|
||||||
|
KeyedSubtree(
|
||||||
|
key: ValueKey("gift_chat_$chatRoomKey"),
|
||||||
|
child: const GiftChatPage(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildImageTab(int index) {
|
Widget _buildImageTab(int index) {
|
||||||
return Tab(
|
return Tab(
|
||||||
height: 32.w,
|
height: 32.w,
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
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';
|
||||||
@ -566,6 +564,7 @@ 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;
|
||||||
|
|
||||||
@ -585,9 +584,15 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
|||||||
|
|
||||||
Future<void> _loadAndPlay() async {
|
Future<void> _loadAndPlay() async {
|
||||||
try {
|
try {
|
||||||
final movieEntity = await _loadMovieEntity(widget.sourceUrl);
|
final movieEntity = await SCGiftVapSvgaManager().acquireSvgaEntity(
|
||||||
if (!mounted || _finished) return;
|
widget.sourceUrl,
|
||||||
|
);
|
||||||
|
if (!mounted || _finished) {
|
||||||
|
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_retainedMovieEntity = movieEntity;
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
_controller.videoItem = movieEntity;
|
_controller.videoItem = movieEntity;
|
||||||
});
|
});
|
||||||
@ -599,28 +604,6 @@ 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;
|
||||||
@ -631,6 +614,8 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2708,13 +2708,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_startRoomStatePolling();
|
_startRoomStatePolling();
|
||||||
final currentContext = context;
|
socialChatRoomManager.fetchContributionLevelData(roomId);
|
||||||
if (currentContext != null && currentContext.mounted) {
|
|
||||||
Provider.of<SocialChatRoomManager>(
|
|
||||||
currentContext,
|
|
||||||
listen: false,
|
|
||||||
).fetchContributionLevelData(roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
failureType = RoomStartupFailureType.rtc;
|
failureType = RoomStartupFailureType.rtc;
|
||||||
final rtcTokenResult = await rtcTokenFuture;
|
final rtcTokenResult = await rtcTokenFuture;
|
||||||
@ -3400,13 +3394,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
void _clearRoomContributionLevelData([BuildContext? targetContext]) {
|
void _clearRoomContributionLevelData([BuildContext? targetContext]) {
|
||||||
final currentContext = targetContext ?? context;
|
final currentContext = targetContext ?? context;
|
||||||
if (currentContext == null || !currentContext.mounted) {
|
resolveSocialChatRoomManager(currentContext).clearContributionLevelData();
|
||||||
return;
|
|
||||||
}
|
|
||||||
Provider.of<SocialChatRoomManager>(
|
|
||||||
currentContext,
|
|
||||||
listen: false,
|
|
||||||
).clearContributionLevelData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -1048,6 +1048,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
lastMsgID: null,
|
lastMsgID: null,
|
||||||
);
|
);
|
||||||
final messages = result.data ?? const <V2TimMessage>[];
|
final messages = result.data ?? const <V2TimMessage>[];
|
||||||
|
if (!_isCurrentVoiceRoomGroup(normalizedGroupId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (final message in messages.reversed) {
|
for (final message in messages.reversed) {
|
||||||
_addRoomHistoryMessage(normalizedGroupId, message);
|
_addRoomHistoryMessage(normalizedGroupId, message);
|
||||||
}
|
}
|
||||||
@ -1722,6 +1725,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isCurrentVoiceRoomGroup(String groupId) {
|
||||||
|
final currentGroupId = _currentVoiceRoomGroupId();
|
||||||
|
return currentGroupId.isNotEmpty && currentGroupId == groupId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
bool _shouldRouteUntrackedRoomRedPacketBroadcast(
|
bool _shouldRouteUntrackedRoomRedPacketBroadcast(
|
||||||
String groupId,
|
String groupId,
|
||||||
V2TimMessage message,
|
V2TimMessage message,
|
||||||
@ -1815,7 +1823,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
if (!_isCurrentVisibleVoiceRoom(payloadRoomId)) {
|
if (!_isCurrentVisibleVoiceRoom(payloadRoomId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_isCurrentUserRoomRedPacketPayload(payload)) {
|
if (_isCurrentUserRoomRedPacketPayload(payload) &&
|
||||||
|
_isDelayedRoomRedPacketPayload(payload)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final dialogData = RoomRedPacketUiData.fromJson(payloadMap);
|
final dialogData = RoomRedPacketUiData.fromJson(payloadMap);
|
||||||
@ -1956,8 +1965,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
///红包触发飘屏
|
///红包触发飘屏
|
||||||
var fData = data["data"];
|
var fData = data["data"];
|
||||||
final payload = _broadcastPayloadMap(fData);
|
final payload = _broadcastPayloadMap(fData);
|
||||||
final packetId = _payloadText(payload['packetId']);
|
|
||||||
final packetRoomId = _payloadText(payload["roomId"]);
|
final packetRoomId = _payloadText(payload["roomId"]);
|
||||||
|
if (isRegionBroadcastGroup &&
|
||||||
|
!_isDelayedRoomRedPacketPayload(fData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_isSameRoomRedPacketRegion(fData)) {
|
if (!_isSameRoomRedPacketRegion(fData)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2554,6 +2566,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
Future<void> sendRoomRedPacketBroadcast(
|
Future<void> sendRoomRedPacketBroadcast(
|
||||||
Map<String, dynamic> payload, {
|
Map<String, dynamic> payload, {
|
||||||
String roomGroupId = '',
|
String roomGroupId = '',
|
||||||
|
bool includeRegionBroadcast = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (payload.isEmpty) {
|
if (payload.isEmpty) {
|
||||||
return;
|
return;
|
||||||
@ -2564,6 +2577,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
targetGroupIds.add(normalizedRoomGroupId);
|
targetGroupIds.add(normalizedRoomGroupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeRegionBroadcast) {
|
||||||
var regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? '';
|
var regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? '';
|
||||||
if (regionGroupId.isEmpty) {
|
if (regionGroupId.isEmpty) {
|
||||||
await syncRoomRedPacketBroadcastGroup();
|
await syncRoomRedPacketBroadcastGroup();
|
||||||
@ -2572,6 +2586,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
if (regionGroupId.isNotEmpty) {
|
if (regionGroupId.isNotEmpty) {
|
||||||
targetGroupIds.add(regionGroupId);
|
targetGroupIds.add(regionGroupId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (targetGroupIds.isEmpty) {
|
if (targetGroupIds.isEmpty) {
|
||||||
return;
|
return;
|
||||||
@ -2620,6 +2635,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
return _isSameRegionBroadcastPayload(payload);
|
return _isSameRegionBroadcastPayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isDelayedRoomRedPacketPayload(dynamic payload) {
|
||||||
|
final map = _broadcastPayloadMap(payload);
|
||||||
|
final packetMode = _payloadText(map['packetMode']).toUpperCase();
|
||||||
|
if (packetMode == 'DELAYED') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (packetMode == 'IMMEDIATE') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _payloadText(map['claimStartTime']).isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
bool _isSameRegionBroadcastPayload(dynamic payload) {
|
bool _isSameRegionBroadcastPayload(dynamic payload) {
|
||||||
final map = _broadcastPayloadMap(payload);
|
final map = _broadcastPayloadMap(payload);
|
||||||
final packetRegion = map['regionCode']?.toString().trim();
|
final packetRegion = map['regionCode']?.toString().trim();
|
||||||
|
|||||||
@ -32,8 +32,12 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeAuthentication() async {
|
Future<void> _initializeAuthentication() async {
|
||||||
|
try {
|
||||||
await SCGoogleAuthService.initialize();
|
await SCGoogleAuthService.initialize();
|
||||||
await _verifyExistingSession();
|
await _verifyExistingSession();
|
||||||
|
} catch (_) {
|
||||||
|
// Login buttons should remain usable even if silent Google restore fails.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _verifyExistingSession() async {
|
Future<void> _verifyExistingSession() async {
|
||||||
@ -51,10 +55,11 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
|||||||
if (authType == SCAuthType.GOOGLE.name) {
|
if (authType == SCAuthType.GOOGLE.name) {
|
||||||
this.authType = authType;
|
this.authType = authType;
|
||||||
try {
|
try {
|
||||||
_user ??= await SCGoogleAuthService.signInWithGoogle();
|
final signedInUser = await SCGoogleAuthService.signInWithGoogle();
|
||||||
if (_user == null) {
|
if (signedInUser == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_user = signedInUser;
|
||||||
uid = _user!.uid;
|
uid = _user!.uid;
|
||||||
SCLoadingManager.show();
|
SCLoadingManager.show();
|
||||||
if (uid.isNotEmpty) {
|
if (uid.isNotEmpty) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:provider/provider.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/tools/sc_loading_manager.dart';
|
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||||
@ -9,6 +10,25 @@ import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart
|
|||||||
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart';
|
||||||
|
|
||||||
|
final SocialChatRoomManager socialChatRoomManager = SocialChatRoomManager();
|
||||||
|
|
||||||
|
SocialChatRoomManager resolveSocialChatRoomManager(BuildContext? context) {
|
||||||
|
if (context != null && context.mounted) {
|
||||||
|
try {
|
||||||
|
final manager = Provider.of<SocialChatRoomManager?>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
if (manager != null) {
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// A stale route context can be outside the provider tree after logout.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return socialChatRoomManager;
|
||||||
|
}
|
||||||
|
|
||||||
class SocialChatRoomManager extends ChangeNotifier {
|
class SocialChatRoomManager extends ChangeNotifier {
|
||||||
MyRoomRes? myRoom;
|
MyRoomRes? myRoom;
|
||||||
SCRoomContributeLevelRes? roomContributeLevelRes;
|
SCRoomContributeLevelRes? roomContributeLevelRes;
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class DataPersistence {
|
|||||||
static bool _isInitialized = false;
|
static bool _isInitialized = false;
|
||||||
static Completer<void>? _initializationCompleter;
|
static Completer<void>? _initializationCompleter;
|
||||||
|
|
||||||
|
static bool get isInitialized => _isInitialized && _prefs != null;
|
||||||
|
|
||||||
// 初始化 SharedPreferences
|
// 初始化 SharedPreferences
|
||||||
static Future<void> initialize() async {
|
static Future<void> initialize() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
@ -48,9 +50,17 @@ class DataPersistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _ensureInitialized() async {
|
||||||
|
if (isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await initialize();
|
||||||
|
_checkInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
// 存储字符串
|
// 存储字符串
|
||||||
static Future<bool> setString(String key, String value) async {
|
static Future<bool> setString(String key, String value) async {
|
||||||
_checkInitialized();
|
await _ensureInitialized();
|
||||||
return await _prefs!.setString(key, value);
|
return await _prefs!.setString(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +72,7 @@ class DataPersistence {
|
|||||||
|
|
||||||
// 存储整数
|
// 存储整数
|
||||||
static Future<bool> setInt(String key, int value) async {
|
static Future<bool> setInt(String key, int value) async {
|
||||||
_checkInitialized();
|
await _ensureInitialized();
|
||||||
return await _prefs!.setInt(key, value);
|
return await _prefs!.setInt(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +84,7 @@ class DataPersistence {
|
|||||||
|
|
||||||
// 存储布尔值
|
// 存储布尔值
|
||||||
static Future<bool> setBool(String key, bool value) async {
|
static Future<bool> setBool(String key, bool value) async {
|
||||||
_checkInitialized();
|
await _ensureInitialized();
|
||||||
return await _prefs!.setBool(key, value);
|
return await _prefs!.setBool(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +96,7 @@ class DataPersistence {
|
|||||||
|
|
||||||
// 存储双精度浮点数
|
// 存储双精度浮点数
|
||||||
static Future<bool> setDouble(String key, double value) async {
|
static Future<bool> setDouble(String key, double value) async {
|
||||||
_checkInitialized();
|
await _ensureInitialized();
|
||||||
return await _prefs!.setDouble(key, value);
|
return await _prefs!.setDouble(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -584,6 +584,9 @@ class OverlayManager {
|
|||||||
if (message.isRegionBroadcast) {
|
if (message.isRegionBroadcast) {
|
||||||
return _shouldDisplayRegionBroadcastMessage(context, message);
|
return _shouldDisplayRegionBroadcastMessage(context, message);
|
||||||
}
|
}
|
||||||
|
if (message.type == 4) {
|
||||||
|
return _isCurrentRoomFloatingMessage(context, message);
|
||||||
|
}
|
||||||
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
||||||
return _isCurrentRoomFloatingMessage(context, message);
|
return _isCurrentRoomFloatingMessage(context, message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,10 @@ class AccountStorage {
|
|||||||
}
|
}
|
||||||
var userJson = DataPersistence.getCurrentUser();
|
var userJson = DataPersistence.getCurrentUser();
|
||||||
if (userJson.isNotEmpty) {
|
if (userJson.isNotEmpty) {
|
||||||
_currentUser = SocialChatLoginRes.fromJson(jsonDecode(userJson));
|
_currentUser = _decodeCurrentUser(userJson);
|
||||||
|
if (_currentUser == null) {
|
||||||
|
unawaited(_clearPersistedSession(clearDurable: false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _currentUser;
|
return _currentUser;
|
||||||
}
|
}
|
||||||
@ -46,16 +49,10 @@ class AccountStorage {
|
|||||||
///同步优化数据
|
///同步优化数据
|
||||||
void setCurrentUser(SocialChatLoginRes user) {
|
void setCurrentUser(SocialChatLoginRes user) {
|
||||||
_currentUser = user;
|
_currentUser = user;
|
||||||
setToken(_currentUser!.token ?? "");
|
token = _currentUser!.token ?? "";
|
||||||
String userJson = jsonEncode(_currentUser?.toJson());
|
String userJson = jsonEncode(_currentUser?.toJson());
|
||||||
if (userJson.isNotEmpty) {
|
if (userJson.isNotEmpty) {
|
||||||
DataPersistence.setCurrentUser(userJson);
|
unawaited(_persistSession(token: token, currentUserJson: userJson));
|
||||||
unawaited(
|
|
||||||
DurableAuthStorage.saveSession(
|
|
||||||
token: _currentUser!.token ?? "",
|
|
||||||
currentUserJson: userJson,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,54 +127,110 @@ class AccountStorage {
|
|||||||
Future<void> restoreDurableSessionIfNeeded() async {
|
Future<void> restoreDurableSessionIfNeeded() async {
|
||||||
final localToken = DataPersistence.getToken().trim();
|
final localToken = DataPersistence.getToken().trim();
|
||||||
final localUserJson = DataPersistence.getCurrentUser().trim();
|
final localUserJson = DataPersistence.getCurrentUser().trim();
|
||||||
if (localToken.isNotEmpty && localUserJson.isNotEmpty) {
|
final localUser = _decodeCurrentUser(localUserJson);
|
||||||
|
if (localToken.isNotEmpty && localUser != null) {
|
||||||
|
token = localToken;
|
||||||
|
_currentUser = localUser;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final session = await DurableAuthStorage.readSession();
|
final session = await DurableAuthStorage.readSession();
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
|
if (localToken.isNotEmpty || localUserJson.isNotEmpty) {
|
||||||
|
unawaited(_clearPersistedSession(clearDurable: false));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
token = session.token;
|
token = session.token;
|
||||||
await DataPersistence.setToken(session.token);
|
_currentUser = _decodeCurrentUser(session.currentUserJson);
|
||||||
await DataPersistence.setCurrentUser(session.currentUserJson);
|
if (_currentUser == null) {
|
||||||
try {
|
token = "";
|
||||||
_currentUser = SocialChatLoginRes.fromJson(
|
await _clearPersistedSession();
|
||||||
jsonDecode(session.currentUserJson),
|
return;
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
_currentUser = null;
|
|
||||||
}
|
}
|
||||||
|
await _persistSession(
|
||||||
|
token: session.token,
|
||||||
|
currentUserJson: session.currentUserJson,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setToken(String tk) {
|
void setToken(String tk) {
|
||||||
token = tk;
|
token = tk;
|
||||||
DataPersistence.setToken(token);
|
unawaited(_persistToken(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cleanUser() {
|
void _cleanUser() {
|
||||||
token = "";
|
token = "";
|
||||||
_currentUser = null;
|
_currentUser = null;
|
||||||
DataPersistence.setToken("");
|
unawaited(_clearPersistedSession());
|
||||||
DataPersistence.setCurrentUser("");
|
|
||||||
unawaited(DataPersistence.clearPendingChannelAuth());
|
unawaited(DataPersistence.clearPendingChannelAuth());
|
||||||
unawaited(DurableAuthStorage.clearSession());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///退出登录
|
///退出登录
|
||||||
void logout(BuildContext context) {
|
void logout(BuildContext context) {
|
||||||
_cleanUser();
|
_cleanUser();
|
||||||
SCHeartbeatUtils.cancelTimer();
|
SCHeartbeatUtils.cancelTimer();
|
||||||
|
try {
|
||||||
Provider.of<RtcProvider>(
|
Provider.of<RtcProvider>(
|
||||||
context,
|
context,
|
||||||
listen: false,
|
listen: false,
|
||||||
).exitCurrentVoiceRoomSession(true);
|
).exitCurrentVoiceRoomSession(true);
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
Provider.of<RtmProvider>(context, listen: false).logout();
|
Provider.of<RtmProvider>(context, listen: false).logout();
|
||||||
|
} catch (_) {}
|
||||||
SCNavigatorUtils.pushLoginIfNeeded(context, clearStack: true);
|
SCNavigatorUtils.pushLoginIfNeeded(context, clearStack: true);
|
||||||
|
try {
|
||||||
SCRoomUtils.closeAllDialogs();
|
SCRoomUtils.closeAllDialogs();
|
||||||
|
} catch (_) {}
|
||||||
SCMessageUtils.redPacketFutureCache.clear();
|
SCMessageUtils.redPacketFutureCache.clear();
|
||||||
SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults();
|
SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults();
|
||||||
|
try {
|
||||||
OverlayManager().dispose();
|
OverlayManager().dispose();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialChatLoginRes? _decodeCurrentUser(String userJson) {
|
||||||
|
if (userJson.trim().isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(userJson);
|
||||||
|
if (decoded is Map) {
|
||||||
|
return SocialChatLoginRes.fromJson(Map<String, dynamic>.from(decoded));
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistToken(String value) async {
|
||||||
|
try {
|
||||||
|
await DataPersistence.setToken(value);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistSession({
|
||||||
|
required String token,
|
||||||
|
required String currentUserJson,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await DataPersistence.setToken(token);
|
||||||
|
await DataPersistence.setCurrentUser(currentUserJson);
|
||||||
|
} catch (_) {}
|
||||||
|
await DurableAuthStorage.saveSession(
|
||||||
|
token: token,
|
||||||
|
currentUserJson: currentUserJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _clearPersistedSession({bool clearDurable = true}) async {
|
||||||
|
try {
|
||||||
|
await DataPersistence.setToken("");
|
||||||
|
await DataPersistence.setCurrentUser("");
|
||||||
|
} catch (_) {}
|
||||||
|
if (clearDurable) {
|
||||||
|
await DurableAuthStorage.clearSession();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,6 +107,7 @@ Future<bool> _isCurrentSessionStillValid(DioException e) async {
|
|||||||
class BaseNetworkClient {
|
class BaseNetworkClient {
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
static const String silentErrorToastKey = 'silentErrorToast';
|
static const String silentErrorToastKey = 'silentErrorToast';
|
||||||
|
static const String suppressAuthLogoutKey = 'suppressAuthLogout';
|
||||||
static const String baseUrlOverrideKey = 'baseUrlOverride';
|
static const String baseUrlOverrideKey = 'baseUrlOverride';
|
||||||
|
|
||||||
BaseNetworkClient() : dio = Dio() {
|
BaseNetworkClient() : dio = Dio() {
|
||||||
@ -292,6 +293,13 @@ class BaseNetworkClient {
|
|||||||
SCLoadingManager.hide();
|
SCLoadingManager.hide();
|
||||||
final bool silentErrorToast =
|
final bool silentErrorToast =
|
||||||
e.requestOptions.extra[BaseNetworkClient.silentErrorToastKey] == true;
|
e.requestOptions.extra[BaseNetworkClient.silentErrorToastKey] == true;
|
||||||
|
final handledBusinessError = await _handleBusinessErrorResponse(
|
||||||
|
e,
|
||||||
|
silentErrorToast: silentErrorToast,
|
||||||
|
);
|
||||||
|
if (handledBusinessError != null) {
|
||||||
|
return handledBusinessError;
|
||||||
|
}
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case DioExceptionType.connectionTimeout:
|
case DioExceptionType.connectionTimeout:
|
||||||
return DioException(requestOptions: e.requestOptions, error: '连接超时');
|
return DioException(requestOptions: e.requestOptions, error: '连接超时');
|
||||||
@ -300,50 +308,10 @@ class BaseNetworkClient {
|
|||||||
case DioExceptionType.receiveTimeout:
|
case DioExceptionType.receiveTimeout:
|
||||||
return DioException(requestOptions: e.requestOptions, error: '接收超时');
|
return DioException(requestOptions: e.requestOptions, error: '接收超时');
|
||||||
case DioExceptionType.badResponse:
|
case DioExceptionType.badResponse:
|
||||||
final responseData = e.response?.data;
|
|
||||||
final errorCode =
|
|
||||||
responseData is Map ? responseData["errorCode"] : null;
|
|
||||||
final errorMsg =
|
|
||||||
responseData is Map
|
|
||||||
? responseData["errorMsg"]
|
|
||||||
: responseData?.toString();
|
|
||||||
if (errorCode == SCErroCode.userNotRegistered.code) {
|
|
||||||
//用户还没有注册
|
|
||||||
SCTts.show("Please register an account first.");
|
|
||||||
BuildContext? context = navigatorKey.currentContext;
|
|
||||||
if (context != null) {
|
|
||||||
SCNavigatorUtils.push(
|
|
||||||
context,
|
|
||||||
LoginRouter.editProfile,
|
|
||||||
replace: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (errorCode == SCErroCode.authUnauthorized.code) {
|
|
||||||
//token过期
|
|
||||||
final shouldLogout =
|
|
||||||
!SCNavigatorUtils.inLoginPage &&
|
|
||||||
!await _isCurrentSessionStillValid(e);
|
|
||||||
final BuildContext? context = navigatorKey.currentContext;
|
|
||||||
if (context != null && context.mounted && shouldLogout) {
|
|
||||||
AccountStorage().logout(context);
|
|
||||||
SCNavigatorUtils.inLoginPage = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (errorMsg.toString().endsWith("balance not made")) {
|
|
||||||
BuildContext? context = navigatorKey.currentContext;
|
|
||||||
if (context != null) {
|
|
||||||
SCRoomUtils.goRecharge(context);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!silentErrorToast) {
|
|
||||||
SCTts.show(errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DioException(
|
return DioException(
|
||||||
requestOptions: e.requestOptions,
|
requestOptions: e.requestOptions,
|
||||||
response: e.response,
|
response: e.response,
|
||||||
error: '服务器错误: $errorCode ${errorMsg ?? ""}'.trim(),
|
error: e.error ?? e.message ?? 'Server fail',
|
||||||
);
|
);
|
||||||
case DioExceptionType.cancel:
|
case DioExceptionType.cancel:
|
||||||
return DioException(
|
return DioException(
|
||||||
@ -362,6 +330,95 @@ class BaseNetworkClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<DioException?> _handleBusinessErrorResponse(
|
||||||
|
DioException e, {
|
||||||
|
required bool silentErrorToast,
|
||||||
|
}) async {
|
||||||
|
final responseData = e.response?.data;
|
||||||
|
if (responseData == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final errorCode = _readErrorCode(responseData);
|
||||||
|
final errorMsg = _readErrorMessage(responseData);
|
||||||
|
if (errorCode == null && errorMsg == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCode == SCErroCode.userNotRegistered.code) {
|
||||||
|
// 用户还没有注册:Google/Apple 登录后需要继续补充资料。
|
||||||
|
SCTts.show("Please register an account first.");
|
||||||
|
final BuildContext? context = navigatorKey.currentContext;
|
||||||
|
if (context != null && context.mounted) {
|
||||||
|
SCNavigatorUtils.push(context, LoginRouter.editProfile, replace: false);
|
||||||
|
}
|
||||||
|
} else if (errorCode == SCErroCode.authUnauthorized.code) {
|
||||||
|
// token过期
|
||||||
|
if (e.requestOptions.extra[BaseNetworkClient.suppressAuthLogoutKey] ==
|
||||||
|
true) {
|
||||||
|
return DioException(
|
||||||
|
requestOptions: e.requestOptions,
|
||||||
|
response: e.response,
|
||||||
|
type: e.type,
|
||||||
|
error: '服务器错误: ${errorCode ?? ""} ${errorMsg ?? ""}'.trim(),
|
||||||
|
message: e.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final shouldLogout =
|
||||||
|
!SCNavigatorUtils.inLoginPage &&
|
||||||
|
!await _isCurrentSessionStillValid(e);
|
||||||
|
final BuildContext? context = navigatorKey.currentContext;
|
||||||
|
if (context != null && context.mounted && shouldLogout) {
|
||||||
|
AccountStorage().logout(context);
|
||||||
|
SCNavigatorUtils.inLoginPage = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final message = errorMsg ?? "";
|
||||||
|
if (message.endsWith("balance not made")) {
|
||||||
|
final BuildContext? context = navigatorKey.currentContext;
|
||||||
|
if (context != null && context.mounted) {
|
||||||
|
SCRoomUtils.goRecharge(context);
|
||||||
|
}
|
||||||
|
} else if (!silentErrorToast && message.isNotEmpty) {
|
||||||
|
SCTts.show(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DioException(
|
||||||
|
requestOptions: e.requestOptions,
|
||||||
|
response: e.response,
|
||||||
|
type: e.type,
|
||||||
|
error: '服务器错误: ${errorCode ?? ""} ${errorMsg ?? ""}'.trim(),
|
||||||
|
message: e.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _readErrorCode(dynamic responseData) {
|
||||||
|
if (responseData is! Map) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final value = responseData["errorCode"] ?? responseData["code"];
|
||||||
|
if (value is int) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is num) {
|
||||||
|
return value.toInt();
|
||||||
|
}
|
||||||
|
if (value is String) {
|
||||||
|
return int.tryParse(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _readErrorMessage(dynamic responseData) {
|
||||||
|
if (responseData is Map) {
|
||||||
|
return responseData["errorMsg"]?.toString() ??
|
||||||
|
responseData["message"]?.toString();
|
||||||
|
}
|
||||||
|
final message = responseData?.toString().trim();
|
||||||
|
return message == null || message.isEmpty ? null : message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotSuccessException implements Exception {
|
class NotSuccessException implements Exception {
|
||||||
|
|||||||
@ -213,6 +213,10 @@ class SCAccountRepository implements SocialChatUserRepository {
|
|||||||
final result = await http.post(
|
final result = await http.post(
|
||||||
"58f9485b5b9f401c6a14bc1e6534122c7a085bb5f6a613094188ca56fe8bc03d",
|
"58f9485b5b9f401c6a14bc1e6534122c7a085bb5f6a613094188ca56fe8bc03d",
|
||||||
data: parm,
|
data: parm,
|
||||||
|
extra: const {
|
||||||
|
BaseNetworkClient.silentErrorToastKey: true,
|
||||||
|
BaseNetworkClient.suppressAuthLogoutKey: true,
|
||||||
|
},
|
||||||
fromJson: (json) => {},
|
fromJson: (json) => {},
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
@ -352,6 +356,10 @@ class SCAccountRepository implements SocialChatUserRepository {
|
|||||||
final result = await http.get(
|
final result = await http.get(
|
||||||
"d28c7d4eeb945fb765c8206c8f79d77f",
|
"d28c7d4eeb945fb765c8206c8f79d77f",
|
||||||
queryParams: {"roomId": roomId},
|
queryParams: {"roomId": roomId},
|
||||||
|
extra: const {
|
||||||
|
BaseNetworkClient.silentErrorToastKey: true,
|
||||||
|
BaseNetworkClient.suppressAuthLogoutKey: true,
|
||||||
|
},
|
||||||
fromJson: (json) => {},
|
fromJson: (json) => {},
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -23,7 +23,8 @@ 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);
|
||||||
Map<String, MovieEntity> videoItemCache = {};
|
final Map<String, MovieEntity> _videoItemCache = <String, MovieEntity>{};
|
||||||
|
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;
|
||||||
@ -79,6 +80,44 @@ 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,
|
||||||
@ -116,7 +155,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);
|
||||||
@ -170,8 +209,9 @@ class SCGiftVapSvgaManager {
|
|||||||
'start preload path=$path active=$_activePreloadCount '
|
'start preload path=$path active=$_activePreloadCount '
|
||||||
'queueRemaining=${_preloadQueue.length}',
|
'queueRemaining=${_preloadQueue.length}',
|
||||||
);
|
);
|
||||||
_warmupPath(path).whenComplete(() {
|
_warmupPath(path).catchError((_) {}).whenComplete(() {
|
||||||
_activePreloadCount--;
|
_activePreloadCount =
|
||||||
|
_activePreloadCount > 0 ? _activePreloadCount - 1 : 0;
|
||||||
_log(
|
_log(
|
||||||
'finish preload path=$path active=$_activePreloadCount '
|
'finish preload path=$path active=$_activePreloadCount '
|
||||||
'queueRemaining=${_preloadQueue.length}',
|
'queueRemaining=${_preloadQueue.length}',
|
||||||
@ -255,17 +295,20 @@ 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) {
|
||||||
videoItemCache.remove(path);
|
final previous = _videoItemCache.remove(path);
|
||||||
videoItemCache[path] = entity;
|
if (previous != null && !identical(previous, entity)) {
|
||||||
_trimResolvedCache(videoItemCache, _maxSvgaCacheEntries);
|
_disposeSvgaEntityIfUnused(previous);
|
||||||
|
}
|
||||||
|
_videoItemCache[path] = entity;
|
||||||
|
_trimSvgaCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _touchCachedPlayablePath(String path) {
|
String? _touchCachedPlayablePath(String path) {
|
||||||
@ -282,6 +325,22 @@ class SCGiftVapSvgaManager {
|
|||||||
_trimResolvedCache(_playablePathCache, _maxPlayablePathCacheEntries);
|
_trimResolvedCache(_playablePathCache, _maxPlayablePathCacheEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _trimSvgaCache() {
|
||||||
|
while (_videoItemCache.length > _maxSvgaCacheEntries) {
|
||||||
|
final removableEntry = _videoItemCache.entries.firstWhere(
|
||||||
|
(entry) => !_isSvgaEntityInUse(entry.value),
|
||||||
|
orElse: () => _videoItemCache.entries.first,
|
||||||
|
);
|
||||||
|
if (_isSvgaEntityInUse(removableEntry.value)) {
|
||||||
|
_videoItemCache.remove(removableEntry.key);
|
||||||
|
_videoItemCache[removableEntry.key] = removableEntry.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_videoItemCache.remove(removableEntry.key);
|
||||||
|
_disposeSvgaEntity(removableEntry.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _trimResolvedCache<T>(Map<String, T> cache, int maxEntries) {
|
void _trimResolvedCache<T>(Map<String, T> cache, int maxEntries) {
|
||||||
while (cache.length > maxEntries) {
|
while (cache.length > maxEntries) {
|
||||||
final oldestKey = cache.keys.first;
|
final oldestKey = cache.keys.first;
|
||||||
@ -554,7 +613,7 @@ class SCGiftVapSvgaManager {
|
|||||||
} else {
|
} else {
|
||||||
_finishCurrentTask();
|
_finishCurrentTask();
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (_) {
|
||||||
_finishCurrentTask();
|
_finishCurrentTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -728,6 +787,41 @@ class SCGiftVapSvgaManager {
|
|||||||
SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback');
|
SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearMemoryCache({bool includeCurrent = false}) {
|
||||||
|
_preloadQueue.clear();
|
||||||
|
_queuedPreloadPaths.clear();
|
||||||
|
_activePreloadCount = 0;
|
||||||
|
final entries = _videoItemCache.entries.toList(growable: false);
|
||||||
|
for (final entry in entries) {
|
||||||
|
final entity = entry.value;
|
||||||
|
if (!includeCurrent && _isSvgaEntityInUse(entity)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_videoItemCache.remove(entry.key);
|
||||||
|
_disposeSvgaEntity(entity);
|
||||||
|
}
|
||||||
|
_playablePathCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSvgaEntityInUse(MovieEntity entity) {
|
||||||
|
return identical(_rsc?.videoItem, entity) ||
|
||||||
|
(_svgaEntityUseCounts[entity] ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposeSvgaEntityIfUnused(MovieEntity entity) {
|
||||||
|
if (_isSvgaEntityInUse(entity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_disposeSvgaEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposeSvgaEntity(MovieEntity entity) {
|
||||||
|
try {
|
||||||
|
_svgaEntityUseCounts.remove(entity);
|
||||||
|
entity.dispose();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
// 释放资源
|
// 释放资源
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}');
|
_log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}');
|
||||||
@ -735,7 +829,7 @@ class SCGiftVapSvgaManager {
|
|||||||
stopPlayback();
|
stopPlayback();
|
||||||
_svgaLoadTasks.clear();
|
_svgaLoadTasks.clear();
|
||||||
_playablePathTasks.clear();
|
_playablePathTasks.clear();
|
||||||
videoItemCache.clear();
|
clearMemoryCache(includeCurrent: true);
|
||||||
_playablePathCache.clear();
|
_playablePathCache.clear();
|
||||||
_rgc?.dispose();
|
_rgc?.dispose();
|
||||||
_rgc = null;
|
_rgc = null;
|
||||||
|
|||||||
@ -55,14 +55,17 @@ class SCHeartbeatUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///上麦主播发送心跳
|
///上麦主播发送心跳
|
||||||
static void scheduleAnchorHeartbeat(String roomId) async {
|
static void scheduleAnchorHeartbeat(String roomId) {
|
||||||
cancelAnchorTimer();
|
cancelAnchorTimer();
|
||||||
scheduleHeartbeat(_c, true, roomId: roomId);
|
unawaited(scheduleHeartbeat(_c, true, roomId: roomId));
|
||||||
SCAccountRepository().anchorHeartbeat(roomId).whenComplete(() {
|
SCAccountRepository()
|
||||||
|
.anchorHeartbeat(roomId)
|
||||||
|
.then((_) {
|
||||||
_a ??= Timer.periodic(Duration(seconds: 60), (timer) {
|
_a ??= Timer.periodic(Duration(seconds: 60), (timer) {
|
||||||
SCAccountRepository().anchorHeartbeat(roomId).catchError((_) => {});
|
SCAccountRepository().anchorHeartbeat(roomId).catchError((_) => {});
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.catchError((_) {});
|
||||||
}
|
}
|
||||||
|
|
||||||
static void cancelAnchorTimer() {
|
static void cancelAnchorTimer() {
|
||||||
|
|||||||
@ -48,6 +48,8 @@ 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;
|
||||||
@ -82,30 +84,43 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查缓存
|
|
||||||
MovieEntity? videoItem;
|
|
||||||
if (widget.useCache) {
|
if (widget.useCache) {
|
||||||
videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource];
|
final videoItem =
|
||||||
|
_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(() {
|
||||||
if (videoItem == null) {
|
_releaseRetainedMovieEntity(except: videoItem);
|
||||||
videoItem =
|
_retainedMovieEntity = 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);
|
||||||
videoItem.autorelease =false;
|
if (!mounted) {
|
||||||
// 存入缓存
|
videoItem.dispose();
|
||||||
if (widget.useCache) {
|
return;
|
||||||
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) {
|
||||||
@ -117,7 +132,6 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
|||||||
if (widget.onFinishLoading != null) {
|
if (widget.onFinishLoading != null) {
|
||||||
widget.onFinishLoading!();
|
widget.onFinishLoading!();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -157,14 +171,16 @@ 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;
|
||||||
SCGiftVapSvgaManager().videoItemCache.remove(widget.resource);
|
_releaseRetainedMovieEntity();
|
||||||
|
SCGiftVapSvgaManager().evictSvgaEntity(widget.resource);
|
||||||
}
|
}
|
||||||
_loadAnimation();
|
_loadAnimation();
|
||||||
}
|
}
|
||||||
@ -173,9 +189,20 @@ 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) {
|
||||||
|
|||||||
@ -270,6 +270,8 @@ 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();
|
||||||
@ -351,34 +353,66 @@ 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) {
|
||||||
videoItem = SCGiftVapSvgaManager().videoItemCache[resource];
|
final videoItem =
|
||||||
|
_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) {
|
// 应用动态数据(文本、图片替换)
|
||||||
videoItem =
|
await _applyDynamicData(videoItem, dynamicData);
|
||||||
|
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);
|
||||||
videoItem.autorelease = false;
|
if (!mounted) {
|
||||||
|
videoItem.dispose();
|
||||||
if (widget.useCache) {
|
return;
|
||||||
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();
|
||||||
@ -386,10 +420,12 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
|||||||
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;
|
||||||
});
|
});
|
||||||
@ -543,6 +579,7 @@ 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;
|
||||||
});
|
});
|
||||||
@ -553,7 +590,9 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
|||||||
void reload() {
|
void reload() {
|
||||||
if (_currentResource != null) {
|
if (_currentResource != null) {
|
||||||
if (widget.useCache) {
|
if (widget.useCache) {
|
||||||
SCGiftVapSvgaManager().videoItemCache.remove(_currentResource);
|
_animationController?.videoItem = null;
|
||||||
|
_releaseRetainedMovieEntity();
|
||||||
|
SCGiftVapSvgaManager().evictSvgaEntity(_currentResource!);
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadAnimation(
|
_loadAnimation(
|
||||||
@ -568,11 +607,22 @@ 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) {
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
|
|||||||
@ -26,6 +26,8 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
|||||||
static final Map<String, num> _claimedAmountByPacketId = {};
|
static final Map<String, num> _claimedAmountByPacketId = {};
|
||||||
static final Set<String> _unavailablePacketIds = {};
|
static final Set<String> _unavailablePacketIds = {};
|
||||||
static final ValueNotifier<int> claimedStateNotifier = ValueNotifier<int>(0);
|
static final ValueNotifier<int> claimedStateNotifier = ValueNotifier<int>(0);
|
||||||
|
static String? _activeDialogPacketId;
|
||||||
|
static DateTime? _activeDialogRequestedAt;
|
||||||
|
|
||||||
final RoomRedPacketUiData data;
|
final RoomRedPacketUiData data;
|
||||||
final List<RoomRedPacketUiData>? dataList;
|
final List<RoomRedPacketUiData>? dataList;
|
||||||
@ -66,6 +68,12 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final nextPacketId = packets[initialIndex].packetId.trim();
|
||||||
|
if (_isDuplicateDialogRequest(nextPacketId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_activeDialogPacketId = nextPacketId;
|
||||||
|
_activeDialogRequestedAt = DateTime.now();
|
||||||
SmartDialog.dismiss(tag: dialogTag);
|
SmartDialog.dismiss(tag: dialogTag);
|
||||||
SmartDialog.show(
|
SmartDialog.show(
|
||||||
tag: dialogTag,
|
tag: dialogTag,
|
||||||
@ -208,6 +216,17 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool _isDuplicateDialogRequest(String packetId) {
|
||||||
|
if (packetId.isEmpty || _activeDialogPacketId != packetId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final requestedAt = _activeDialogRequestedAt;
|
||||||
|
final isRecent =
|
||||||
|
requestedAt != null &&
|
||||||
|
DateTime.now().difference(requestedAt) < const Duration(seconds: 1);
|
||||||
|
return isRecent || SmartDialog.checkExist(tag: dialogTag);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RoomRedPacketOpenDialog> createState() =>
|
State<RoomRedPacketOpenDialog> createState() =>
|
||||||
_RoomRedPacketOpenDialogState();
|
_RoomRedPacketOpenDialogState();
|
||||||
|
|||||||
@ -29,6 +29,8 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
|||||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_error.dart';
|
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_error.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_history_page.dart';
|
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_history_page.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_rule_page.dart';
|
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_rule_page.dart';
|
||||||
|
|
||||||
class RoomRedPacketSendPanel extends StatefulWidget {
|
class RoomRedPacketSendPanel extends StatefulWidget {
|
||||||
@ -664,7 +666,8 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
|
|||||||
currentUser?.userNickname ?? result.userName ?? 'Lucky Pack';
|
currentUser?.userNickname ?? result.userName ?? 'Lucky Pack';
|
||||||
final senderAvatar = currentUser?.userAvatar ?? result.userAvatar ?? '';
|
final senderAvatar = currentUser?.userAvatar ?? result.userAvatar ?? '';
|
||||||
final claimStartTime = (result.claimStartTime ?? '').trim();
|
final claimStartTime = (result.claimStartTime ?? '').trim();
|
||||||
if (_isDelayedRedPacketResult(result) && claimStartTime.isEmpty) {
|
final isDelayedPacket = _isDelayedRedPacketResult(result);
|
||||||
|
if (isDelayedPacket && claimStartTime.isEmpty) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[RoomRedPacket][SendFallback] skip delayed local UI because server '
|
'[RoomRedPacket][SendFallback] skip delayed local UI because server '
|
||||||
'claimStartTime is empty packetId=$packetId',
|
'claimStartTime is empty packetId=$packetId',
|
||||||
@ -718,6 +721,7 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
|
|||||||
rtmProvider.sendRoomRedPacketBroadcast(
|
rtmProvider.sendRoomRedPacketBroadcast(
|
||||||
payload,
|
payload,
|
||||||
roomGroupId: roomProfile?.roomAccount ?? '',
|
roomGroupId: roomProfile?.roomAccount ?? '',
|
||||||
|
includeRegionBroadcast: isDelayedPacket,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
OverlayManager().addMessage(
|
OverlayManager().addMessage(
|
||||||
@ -730,15 +734,35 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
|
|||||||
toUserId: packetId,
|
toUserId: packetId,
|
||||||
coins: result.totalAmount ?? _selectedAmount,
|
coins: result.totalAmount ?? _selectedAmount,
|
||||||
number: result.totalCount ?? _selectedCount,
|
number: result.totalCount ?? _selectedCount,
|
||||||
broadcastScope: SCFloatingMessage.broadcastScopeRegion,
|
broadcastScope:
|
||||||
|
isDelayedPacket ? SCFloatingMessage.broadcastScopeRegion : '',
|
||||||
priority: 1000,
|
priority: 1000,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!isDelayedPacket) {
|
||||||
|
_showImmediateSentRedPacketDialog(payload);
|
||||||
|
}
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[RoomRedPacket][SendFallback] inserted local chat/floating packetId=$packetId roomId=$roomId',
|
'[RoomRedPacket][SendFallback] inserted local chat/floating packetId=$packetId roomId=$roomId',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showImmediateSentRedPacketDialog(Map<String, dynamic> payload) {
|
||||||
|
final dialogData = RoomRedPacketUiData.fromJson(payload);
|
||||||
|
if (!dialogData.hasPacketId ||
|
||||||
|
!dialogData.isOpenable ||
|
||||||
|
RoomRedPacketOpenDialog.isClaimedLocally(dialogData.packetId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final currentContext = navigatorKey.currentState?.context;
|
||||||
|
if (currentContext == null || !currentContext.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RoomRedPacketOpenDialog.show(currentContext, dialogData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _showInsufficientBalanceRechargeSheet() {
|
void _showInsufficientBalanceRechargeSheet() {
|
||||||
SmartDialog.dismiss(tag: RoomRedPacketSendPanel.dialogTag);
|
SmartDialog.dismiss(tag: RoomRedPacketSendPanel.dialogTag);
|
||||||
_RoomRedPacketRechargeSheet.show(context);
|
_RoomRedPacketRechargeSheet.show(context);
|
||||||
|
|||||||
@ -52,6 +52,8 @@ 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
|
||||||
@ -78,9 +80,10 @@ 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;
|
||||||
}
|
}
|
||||||
@ -89,37 +92,53 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
|||||||
_hasError = false;
|
_hasError = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
MovieEntity? acquiredMovieEntity;
|
||||||
try {
|
try {
|
||||||
final cache = SCGiftVapSvgaManager().videoItemCache;
|
final movieEntity =
|
||||||
var movieEntity = cache[resource];
|
_retainedResource == resource && _retainedMovieEntity != null
|
||||||
if (movieEntity == null) {
|
? _retainedMovieEntity!
|
||||||
movieEntity =
|
: await SCGiftVapSvgaManager().acquireSvgaEntity(resource);
|
||||||
resource.startsWith("http")
|
final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity);
|
||||||
? await SVGAParser.shared.decodeFromURL(resource)
|
if (acquiredNewEntity) {
|
||||||
: await SVGAParser.shared.decodeFromAssets(resource);
|
acquiredMovieEntity = movieEntity;
|
||||||
movieEntity.autorelease = false;
|
}
|
||||||
cache[resource] = movieEntity;
|
if (!mounted || widget.resource.trim() != resource) {
|
||||||
|
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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,9 +164,20 @@ 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;
|
||||||
|
|||||||
@ -8,9 +8,11 @@ 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,
|
||||||
@ -49,8 +51,26 @@ 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 = _cache[assetPath];
|
final cached = _touchCachedMovieEntity(assetPath);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@ -71,7 +91,7 @@ class SCSvgaAssetWidget extends StatefulWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
entity.autorelease = false;
|
entity.autorelease = false;
|
||||||
_cache[assetPath] = entity;
|
_cacheMovieEntity(assetPath, entity);
|
||||||
return entity;
|
return entity;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
@ -83,6 +103,77 @@ 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();
|
||||||
}
|
}
|
||||||
@ -91,6 +182,8 @@ 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
|
||||||
@ -131,25 +224,44 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_hasError = false;
|
_hasError = false;
|
||||||
});
|
});
|
||||||
|
MovieEntity? acquiredMovieEntity;
|
||||||
try {
|
try {
|
||||||
final movieEntity = await SCSvgaAssetWidget._obtainMovieEntity(assetPath);
|
final movieEntity =
|
||||||
|
_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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -185,9 +297,20 @@ 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) {
|
||||||
|
|||||||
@ -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.0+11
|
version: 1.3.1+12
|
||||||
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
26
test/account_storage_session_test.dart
Normal file
26
test/account_storage_session_test.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
DataPersistence.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('corrupted cached user session is ignored and cleared', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'token': 'stale-token',
|
||||||
|
'currentUser': '{bad json',
|
||||||
|
});
|
||||||
|
await DataPersistence.initialize();
|
||||||
|
|
||||||
|
expect(AccountStorage().getCurrentUser(), isNull);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
|
expect(DataPersistence.getToken(), isEmpty);
|
||||||
|
expect(DataPersistence.getCurrentUser(), isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user