修复cp问题
This commit is contained in:
parent
79e78cd897
commit
2999de23d1
@ -1868,14 +1868,7 @@ class _MessageItem extends StatelessWidget {
|
|||||||
|
|
||||||
SCSystemInvitMessageRes? _cpInviteFromMessageData(dynamic data) {
|
SCSystemInvitMessageRes? _cpInviteFromMessageData(dynamic data) {
|
||||||
try {
|
try {
|
||||||
final content = data is Map ? data["content"] : null;
|
return SCSystemInvitMessageRes.fromMessageData(data);
|
||||||
if (content is String && content.trim().isNotEmpty) {
|
|
||||||
final decoded = jsonDecode(content);
|
|
||||||
return SCSystemInvitMessageRes.fromJson(decoded);
|
|
||||||
}
|
|
||||||
if (content is Map) {
|
|
||||||
return SCSystemInvitMessageRes.fromJson(content);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_cpMessageLog('parse invite failed error=$error');
|
_cpMessageLog('parse invite failed error=$error');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1843,14 +1843,7 @@ class _MessageItem extends StatelessWidget {
|
|||||||
|
|
||||||
SCSystemInvitMessageRes? _cpInviteFromMessageData(dynamic data) {
|
SCSystemInvitMessageRes? _cpInviteFromMessageData(dynamic data) {
|
||||||
try {
|
try {
|
||||||
final content = data is Map ? data["content"] : null;
|
return SCSystemInvitMessageRes.fromMessageData(data);
|
||||||
if (content is String && content.trim().isNotEmpty) {
|
|
||||||
final decoded = jsonDecode(content);
|
|
||||||
return SCSystemInvitMessageRes.fromJson(decoded);
|
|
||||||
}
|
|
||||||
if (content is Map) {
|
|
||||||
return SCSystemInvitMessageRes.fromJson(content);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,10 +18,9 @@ import 'package:yumi/app/config/business_logic_strategy.dart';
|
|||||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_system_invit_message_res.dart';
|
|
||||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||||
import 'package:yumi/main.dart';
|
import 'package:yumi/main.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -84,6 +83,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
TabController? _tabController;
|
TabController? _tabController;
|
||||||
List<String> _tabTypes = <String>[];
|
List<String> _tabTypes = <String>[];
|
||||||
bool _didApplyInitialTab = false;
|
bool _didApplyInitialTab = false;
|
||||||
|
bool _showFirstRechargeGiftEntry = false;
|
||||||
|
|
||||||
/// 业务逻辑策略访问器
|
/// 业务逻辑策略访问器
|
||||||
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
||||||
@ -240,6 +240,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
listen: false,
|
listen: false,
|
||||||
).giftBackpack(forceRefresh: true),
|
).giftBackpack(forceRefresh: true),
|
||||||
);
|
);
|
||||||
|
unawaited(_loadFirstRechargeGiftEntryState());
|
||||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||||
rtcProvider?.roomWheatMap.forEach((k, v) {
|
rtcProvider?.roomWheatMap.forEach((k, v) {
|
||||||
if (v.user != null) {
|
if (v.user != null) {
|
||||||
@ -275,6 +276,35 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
_normalizeCpRecipientSelection(notify: true);
|
_normalizeCpRecipientSelection(notify: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFirstRechargeGiftEntryState() async {
|
||||||
|
if (SCGlobalConfig.isReview || AccountStorage().getToken().isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final home = await SCConfigRepositoryImp().firstRechargeRewardHome();
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final shouldShow = home.shouldShowRewardDialog;
|
||||||
|
if (_showFirstRechargeGiftEntry == shouldShow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_showFirstRechargeGiftEntry = shouldShow;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('[FirstRecharge][gift] load entry failed: $error');
|
||||||
|
}
|
||||||
|
if (!mounted || !_showFirstRechargeGiftEntry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_showFirstRechargeGiftEntry = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool get _isCurrentCpGiftTab {
|
bool get _isCurrentCpGiftTab {
|
||||||
final controller = _tabController;
|
final controller = _tabController;
|
||||||
if (controller == null || controller.index >= _tabTypes.length) {
|
if (controller == null || controller.index >= _tabTypes.length) {
|
||||||
@ -287,9 +317,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
|
|
||||||
bool _isCpGift(SocialChatGiftRes? gift) => scIsCpGift(gift);
|
bool _isCpGift(SocialChatGiftRes? gift) => scIsCpGift(gift);
|
||||||
|
|
||||||
String _cpRelationTypeForGift(SocialChatGiftRes gift) =>
|
|
||||||
scCpRelationTypeForGift(gift);
|
|
||||||
|
|
||||||
bool get _isCpRecipientSingleMode =>
|
bool get _isCpRecipientSingleMode =>
|
||||||
_isCurrentCpGiftTab || _isCpGift(checkedGift);
|
_isCurrentCpGiftTab || _isCpGift(checkedGift);
|
||||||
|
|
||||||
@ -703,10 +730,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
final currentTabType = giftTabs[tabController.index].type;
|
final currentTabType = giftTabs[tabController.index].type;
|
||||||
final isCpTab = currentTabType == "CP";
|
final isCpTab = currentTabType == "CP";
|
||||||
final showFirstRechargeEntry =
|
final showFirstRechargeEntry =
|
||||||
!SCGlobalConfig.isReview &&
|
!SCGlobalConfig.isReview && !isCpTab && _showFirstRechargeGiftEntry;
|
||||||
!isCpTab &&
|
|
||||||
(AccountStorage().getCurrentUser()?.userProfile?.firstRecharge ??
|
|
||||||
false);
|
|
||||||
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
|
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
|
||||||
final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift);
|
final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift);
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@ -1577,7 +1601,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
await _refreshGiftBackpack();
|
await _refreshGiftBackpack();
|
||||||
_refreshCpStateAfterGift(request, profileManager);
|
_refreshCpStateAfterGift(request, profileManager);
|
||||||
_sendCpInviteMessageAfterGift(request);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1640,7 +1663,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
profileManager?.updateBalance(result);
|
profileManager?.updateBalance(result);
|
||||||
_refreshCpStateAfterGift(request, profileManager);
|
_refreshCpStateAfterGift(request, profileManager);
|
||||||
_sendCpInviteMessageAfterGift(request);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final errorMessage =
|
final errorMessage =
|
||||||
request.isBackpackGiftRequest && _isBackpackQuantityError(e)
|
request.isBackpackGiftRequest && _isBackpackQuantityError(e)
|
||||||
@ -1683,90 +1705,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
unawaited(profileManager?.refreshLoadedCpProfiles());
|
unawaited(profileManager?.refreshLoadedCpProfiles());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendCpInviteMessageAfterGift(_GiftSendRequest request) {
|
|
||||||
if (!_isCpGift(request.gift)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (request.acceptUserIds.length != 1 || request.acceptUsers.length != 1) {
|
|
||||||
_giftFxLog(
|
|
||||||
'skip relation invite C2C reason=invalid_accept_count '
|
|
||||||
'acceptUserIds=${request.acceptUserIds.join(",")}',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unawaited(_sendCpInviteMessageAfterGiftAsync(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _sendCpInviteMessageAfterGiftAsync(
|
|
||||||
_GiftSendRequest request,
|
|
||||||
) async {
|
|
||||||
final relationType = _cpRelationTypeForGift(request.gift);
|
|
||||||
final receiverProfile = request.acceptUsers.first.user;
|
|
||||||
final receiverId =
|
|
||||||
(receiverProfile?.id ?? request.acceptUserIds.first).trim();
|
|
||||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
|
||||||
if (receiverId.isEmpty || currentProfile == null) {
|
|
||||||
_giftFxLog(
|
|
||||||
'skip relation invite C2C reason=missing_user '
|
|
||||||
'receiverId=$receiverId relationType=$relationType '
|
|
||||||
'hasCurrent=${currentProfile != null}',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final providerContext = navigatorKey.currentState?.context ?? context;
|
|
||||||
final rtmProvider = Provider.of<RtmProvider>(
|
|
||||||
providerContext,
|
|
||||||
listen: false,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
final applyId = await SCAccountRepository().cpRelationshipApplyId(
|
|
||||||
receiverId,
|
|
||||||
relationType: relationType,
|
|
||||||
);
|
|
||||||
if ((applyId ?? "").trim().isEmpty) {
|
|
||||||
_giftFxLog(
|
|
||||||
'skip relation invite C2C reason=no_apply_id '
|
|
||||||
'receiverId=$receiverId relationType=$relationType',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final expireAtSeconds =
|
|
||||||
DateTime.now().millisecondsSinceEpoch ~/ 1000 + 86400;
|
|
||||||
final giftCover =
|
|
||||||
(request.gift.giftPhoto ?? "").trim().isNotEmpty
|
|
||||||
? request.gift.giftPhoto
|
|
||||||
: request.gift.giftSourceUrl;
|
|
||||||
final invite = SCSystemInvitMessageRes(
|
|
||||||
account: currentProfile.account ?? currentProfile.id,
|
|
||||||
actualAccount: currentProfile.id,
|
|
||||||
userAvatar: currentProfile.userAvatar ?? "",
|
|
||||||
userHeaddress: currentProfile.getHeaddress()?.sourceUrl,
|
|
||||||
userHeaddressCover: currentProfile.getHeaddress()?.cover,
|
|
||||||
userNickname: currentProfile.userNickname ?? "",
|
|
||||||
relationType: relationType,
|
|
||||||
dismissEndTime: expireAtSeconds,
|
|
||||||
giftCover: giftCover,
|
|
||||||
giftCount: request.quantity,
|
|
||||||
);
|
|
||||||
_giftFxLog(
|
|
||||||
'send relation invite C2C applyId=$applyId '
|
|
||||||
'receiverId=$receiverId relationType=$relationType '
|
|
||||||
'giftId=${request.gift.id}',
|
|
||||||
);
|
|
||||||
await rtmProvider.sendCpInviteBuildMessage(
|
|
||||||
applyId: applyId!,
|
|
||||||
invite: invite,
|
|
||||||
receiverId: receiverId,
|
|
||||||
receiverProfile: receiverProfile,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
_giftFxLog(
|
|
||||||
'send relation invite C2C failed '
|
|
||||||
'receiverId=$receiverId relationType=$relationType error=$error',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<MicRes>> _giveBackpackGiftToRecipients(
|
Future<List<MicRes>> _giveBackpackGiftToRecipients(
|
||||||
SCChatRoomRepository repository,
|
SCChatRoomRepository repository,
|
||||||
_GiftSendRequest request,
|
_GiftSendRequest request,
|
||||||
|
|||||||
@ -15,7 +15,9 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_dialog_utils.dart';
|
||||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
@ -50,6 +52,7 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
Locale? _lastLocale;
|
Locale? _lastLocale;
|
||||||
bool _hasShownEntryDialogs = false;
|
bool _hasShownEntryDialogs = false;
|
||||||
bool _hasShownRegisterRewardDialog = false;
|
bool _hasShownRegisterRewardDialog = false;
|
||||||
|
bool _hasRequestedFirstRechargeDialog = false;
|
||||||
bool _isShowingDailySignInDialog = false;
|
bool _isShowingDailySignInDialog = false;
|
||||||
bool _showRegisterRewardAfterDailySignIn = false;
|
bool _showRegisterRewardAfterDailySignIn = false;
|
||||||
bool _isOpeningFirstRegisterRoomGame = false;
|
bool _isOpeningFirstRegisterRoomGame = false;
|
||||||
@ -438,14 +441,27 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _showDailySignInDialogIfNeeded();
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _showEntryPopupIfNeeded();
|
await _showEntryPopupIfNeeded();
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _showFirstRechargeDialogIfNeeded();
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _openFirstRegisterRoomGameIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showDailySignInDialogIfNeeded() async {
|
||||||
final dialogData = await _loadDailySignInDialogData();
|
final dialogData = await _loadDailySignInDialogData();
|
||||||
if (!mounted || dialogData == null) {
|
if (!mounted || dialogData == null) {
|
||||||
await _openFirstRegisterRoomGameIfNeeded();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_isShowingDailySignInDialog = true;
|
_isShowingDailySignInDialog = true;
|
||||||
@ -461,7 +477,6 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _openFirstRegisterRoomGameIfNeeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showEntryPopupIfNeeded() async {
|
Future<void> _showEntryPopupIfNeeded() async {
|
||||||
@ -474,6 +489,18 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showFirstRechargeDialogIfNeeded() async {
|
||||||
|
if (_hasRequestedFirstRechargeDialog ||
|
||||||
|
SCGlobalConfig.isReview ||
|
||||||
|
AccountStorage().getToken().isEmpty ||
|
||||||
|
!mounted ||
|
||||||
|
!(ModalRoute.of(context)?.isCurrent ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hasRequestedFirstRechargeDialog = true;
|
||||||
|
await SCDialogUtils.showFirstRechargeDialog(context);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _waitForRegisterRewardSocketIfNeeded() async {
|
Future<void> _waitForRegisterRewardSocketIfNeeded() async {
|
||||||
if (!DataPersistence.getAwaitRegisterRewardSocket() ||
|
if (!DataPersistence.getAwaitRegisterRewardSocket() ||
|
||||||
DataPersistence.getPendingRegisterRewardDialog()) {
|
DataPersistence.getPendingRegisterRewardDialog()) {
|
||||||
|
|||||||
@ -79,6 +79,8 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
int _shownRoomStartupFailureToken = 0;
|
int _shownRoomStartupFailureToken = 0;
|
||||||
RtcProvider? _rtcProvider;
|
RtcProvider? _rtcProvider;
|
||||||
bool _roomProviderRebuildScheduled = false;
|
bool _roomProviderRebuildScheduled = false;
|
||||||
|
bool _roomRouteVisibleScheduled = false;
|
||||||
|
bool _roomVisualEffectsEnsureScheduled = false;
|
||||||
ModalRoute<dynamic>? _routeObserverRoute;
|
ModalRoute<dynamic>? _routeObserverRoute;
|
||||||
SCRoomTopLayerGuardLease? _coveredRouteGuardLease;
|
SCRoomTopLayerGuardLease? _coveredRouteGuardLease;
|
||||||
bool _roomRouteVisible = false;
|
bool _roomRouteVisible = false;
|
||||||
@ -87,14 +89,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tabController = TabController(length: _chatTabCount, vsync: this);
|
_tabController = TabController(length: _chatTabCount, vsync: this);
|
||||||
Provider.of<RtcProvider>(
|
|
||||||
context,
|
|
||||||
listen: false,
|
|
||||||
).setVoiceRoomRouteVisible(true);
|
|
||||||
SCRoomTopLayerGuard().activeListenable.addListener(
|
SCRoomTopLayerGuard().activeListenable.addListener(
|
||||||
_handleRoomTopLayerGuardChanged,
|
_handleRoomTopLayerGuardChanged,
|
||||||
);
|
);
|
||||||
_enableRoomVisualEffects();
|
|
||||||
_tabController.addListener(_handleTabChange);
|
_tabController.addListener(_handleTabChange);
|
||||||
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||||
event,
|
event,
|
||||||
@ -131,12 +128,12 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
_routeObserverRoute = route;
|
_routeObserverRoute = route;
|
||||||
modalRouteObserver.subscribe(this, route);
|
modalRouteObserver.subscribe(this, route);
|
||||||
if (route.isCurrent) {
|
if (route.isCurrent) {
|
||||||
_handleRoomRouteVisible();
|
_scheduleRoomRouteVisible();
|
||||||
} else {
|
} else {
|
||||||
_handleRoomRouteCovered();
|
_handleRoomRouteCovered();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ensureRoomVisualEffectsEnabled();
|
_scheduleEnsureRoomVisualEffectsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -184,6 +181,21 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
_setRoomRouteVisible(true);
|
_setRoomRouteVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scheduleRoomRouteVisible() {
|
||||||
|
if (_roomRouteVisibleScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_roomRouteVisibleScheduled = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_roomRouteVisibleScheduled = false;
|
||||||
|
if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_handleRoomRouteVisible();
|
||||||
|
_ensureRoomVisualEffectsEnabled();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _handleRoomRouteCovered() {
|
void _handleRoomRouteCovered() {
|
||||||
_setRoomRouteVisible(false);
|
_setRoomRouteVisible(false);
|
||||||
_acquireCoveredRouteGuard();
|
_acquireCoveredRouteGuard();
|
||||||
@ -260,6 +272,20 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
_enableRoomVisualEffects();
|
_enableRoomVisualEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scheduleEnsureRoomVisualEffectsEnabled() {
|
||||||
|
if (_roomVisualEffectsEnsureScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_roomVisualEffectsEnsureScheduled = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_roomVisualEffectsEnsureScheduled = false;
|
||||||
|
if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ensureRoomVisualEffectsEnabled();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _clearTransientRoomVisualEffects({required String reason}) {
|
void _clearTransientRoomVisualEffects({required String reason}) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
|||||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||||
import 'package:yumi/app/routes/sc_routes.dart';
|
import 'package:yumi/app/routes/sc_routes.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||||
import 'package:yumi/shared/tools/sc_lk_event_bus.dart';
|
import 'package:yumi/shared/tools/sc_lk_event_bus.dart';
|
||||||
import 'package:yumi/shared/tools/sc_pick_utils.dart';
|
import 'package:yumi/shared/tools/sc_pick_utils.dart';
|
||||||
@ -1476,7 +1477,9 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _profileRelationDays(CPRes relation) {
|
String _profileRelationDays(CPRes relation) {
|
||||||
return _firstNonBlankProfileValue([relation.days, "0"]);
|
return scCpRelationDisplayDays(
|
||||||
|
_firstNonBlankProfileValue([relation.days, "0"]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _profileRelationLevelDisplayText(String rawText, CPRes relation) {
|
String _profileRelationLevelDisplayText(String rawText, CPRes relation) {
|
||||||
|
|||||||
@ -308,6 +308,18 @@ class _PersistedVoiceRoomCleanupRequest {
|
|||||||
final bool clearMarkerOnly;
|
final bool clearMarkerOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RoomMicRelationProfileCache {
|
||||||
|
const _RoomMicRelationProfileCache({
|
||||||
|
required this.profile,
|
||||||
|
required this.loadedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SocialChatUserProfile profile;
|
||||||
|
final DateTime loadedAt;
|
||||||
|
|
||||||
|
bool isExpired(DateTime now, Duration ttl) => now.difference(loadedAt) > ttl;
|
||||||
|
}
|
||||||
|
|
||||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||||
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
|
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
|
||||||
static const String _roomRocketPagLaunchDialogTag =
|
static const String _roomRocketPagLaunchDialogTag =
|
||||||
@ -327,6 +339,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
Duration(milliseconds: 1800),
|
Duration(milliseconds: 1800),
|
||||||
Duration(milliseconds: 3200),
|
Duration(milliseconds: 3200),
|
||||||
];
|
];
|
||||||
|
static const List<String> _roomMicRelationTypes = [
|
||||||
|
'CP',
|
||||||
|
'BROTHER',
|
||||||
|
'SISTERS',
|
||||||
|
];
|
||||||
|
static const Duration _roomMicRelationCacheTtl = Duration(minutes: 5);
|
||||||
static const int _roomPasswordNotTrueErrorCode = 9005;
|
static const int _roomPasswordNotTrueErrorCode = 9005;
|
||||||
|
|
||||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||||
@ -415,6 +433,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
Map<num, MicRes> roomWheatMap = {};
|
Map<num, MicRes> roomWheatMap = {};
|
||||||
final Map<num, int> _seatEmojiEventVersions = {};
|
final Map<num, int> _seatEmojiEventVersions = {};
|
||||||
int _emojiPlaybackEventVersion = 0;
|
int _emojiPlaybackEventVersion = 0;
|
||||||
|
final Map<String, _RoomMicRelationProfileCache> _roomMicRelationCache =
|
||||||
|
<String, _RoomMicRelationProfileCache>{};
|
||||||
|
final Set<String> _roomMicRelationLoadingUserIds = <String>{};
|
||||||
final Set<String> _roomMusicPublishingUserIds = <String>{};
|
final Set<String> _roomMusicPublishingUserIds = <String>{};
|
||||||
final Map<String, Timer> _roomMusicPublishingClearTimers = <String, Timer>{};
|
final Map<String, Timer> _roomMusicPublishingClearTimers = <String, Timer>{};
|
||||||
|
|
||||||
@ -1428,13 +1449,266 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}) {
|
}) {
|
||||||
final changed = !_sameMicMaps(roomWheatMap, nextMap);
|
final changed = !_sameMicMaps(roomWheatMap, nextMap);
|
||||||
roomWheatMap = nextMap;
|
roomWheatMap = nextMap;
|
||||||
|
final relationCacheChanged = _mergeCachedRoomMicRelationProfiles();
|
||||||
|
_scheduleRoomMicRelationHydration();
|
||||||
_syncSelfMicRuntimeState();
|
_syncSelfMicRuntimeState();
|
||||||
final roomMusicChanged = _pruneRoomMusicPublishingUsersToMicSeats();
|
final roomMusicChanged = _pruneRoomMusicPublishingUsersToMicSeats();
|
||||||
if (changed || notifyIfUnchanged || roomMusicChanged) {
|
if (changed ||
|
||||||
|
notifyIfUnchanged ||
|
||||||
|
roomMusicChanged ||
|
||||||
|
relationCacheChanged) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _mergeCachedRoomMicRelationProfiles() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
_roomMicRelationCache.removeWhere(
|
||||||
|
(_, cache) => cache.isExpired(now, _roomMicRelationCacheTtl),
|
||||||
|
);
|
||||||
|
var changed = false;
|
||||||
|
final mergedMap = roomWheatMap.map((index, mic) {
|
||||||
|
final user = mic.user;
|
||||||
|
final userId = _roomMicRelationUserId(user);
|
||||||
|
if (user == null || userId.isEmpty) {
|
||||||
|
return MapEntry(index, mic);
|
||||||
|
}
|
||||||
|
final cache = _roomMicRelationCache[userId];
|
||||||
|
if (cache == null || cache.isExpired(now, _roomMicRelationCacheTtl)) {
|
||||||
|
return MapEntry(index, mic);
|
||||||
|
}
|
||||||
|
final mergedUser = _mergeRoomMicRelationProfile(user, cache.profile);
|
||||||
|
if (identical(mergedUser, user)) {
|
||||||
|
return MapEntry(index, mic);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return MapEntry(index, mic.copyWith(user: mergedUser));
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
roomWheatMap = mergedMap;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleRoomMicRelationHydration() {
|
||||||
|
final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||||
|
if (roomId.isEmpty || roomWheatMap.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final now = DateTime.now();
|
||||||
|
final queuedUserIds = <String>{};
|
||||||
|
for (final mic in roomWheatMap.values) {
|
||||||
|
final user = mic.user;
|
||||||
|
final userId = _roomMicRelationUserId(user);
|
||||||
|
if (user == null || userId.isEmpty || !queuedUserIds.add(userId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final cache = _roomMicRelationCache[userId];
|
||||||
|
if (cache == null && _hasCompleteRoomMicRelationLists(user)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cache != null && !cache.isExpired(now, _roomMicRelationCacheTtl)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!_roomMicRelationLoadingUserIds.add(userId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unawaited(_hydrateRoomMicRelationProfile(roomId, userId, user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _hydrateRoomMicRelationProfile(
|
||||||
|
String roomId,
|
||||||
|
String userId,
|
||||||
|
SocialChatUserProfile profile,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final hydratedProfile = await _loadRoomMicRelationProfile(
|
||||||
|
profile,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
_roomMicRelationCache[userId] = _RoomMicRelationProfileCache(
|
||||||
|
profile: hydratedProfile,
|
||||||
|
loadedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
_applyHydratedRoomMicRelationProfile(roomId, userId, hydratedProfile);
|
||||||
|
} finally {
|
||||||
|
_roomMicRelationLoadingUserIds.remove(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SocialChatUserProfile> _loadRoomMicRelationProfile(
|
||||||
|
SocialChatUserProfile profile,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
final cpPairs = <CPRes>[];
|
||||||
|
final closeFriendPairs = <CPRes>[];
|
||||||
|
await Future.wait(
|
||||||
|
_roomMicRelationTypes.map((relationType) async {
|
||||||
|
final cabin = await _safeLoadRoomMicRelationCabin(userId, relationType);
|
||||||
|
final pair = cabin?.cpPairUserProfileCO;
|
||||||
|
if (!_hasRoomMicRelationPair(pair)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final normalizedType = scNormalizeCpRelationType(
|
||||||
|
pair?.relationType ?? relationType,
|
||||||
|
);
|
||||||
|
final enrichedPair = pair!.copyWith(
|
||||||
|
relationType: normalizedType,
|
||||||
|
cpValue: pair.cpValue ?? cabin?.cpVal?.toDouble(),
|
||||||
|
levelText:
|
||||||
|
pair.levelText ?? _roomMicRelationLevelTextFromCabin(cabin),
|
||||||
|
);
|
||||||
|
if (normalizedType == "CP") {
|
||||||
|
cpPairs.add(enrichedPair);
|
||||||
|
} else {
|
||||||
|
closeFriendPairs.add(enrichedPair);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return profile.copyWith(
|
||||||
|
cpList: cpPairs,
|
||||||
|
closeFriendList: closeFriendPairs,
|
||||||
|
isCpRelation: cpPairs.isNotEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SCCpCabinRes?> _safeLoadRoomMicRelationCabin(
|
||||||
|
String userId,
|
||||||
|
String relationType,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await SCAccountRepository().cpCabin(
|
||||||
|
userId,
|
||||||
|
relationType: relationType,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
"[CP][MicRelation] cabin failed userId=$userId "
|
||||||
|
"relationType=$relationType error=$error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _roomMicRelationLevelTextFromCabin(SCCpCabinRes? cabin) {
|
||||||
|
final config = cabin?.currentCpCabinConfigCO;
|
||||||
|
if (config is! Map) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final level = _firstRoomMicRelationConfigText([
|
||||||
|
config["level"],
|
||||||
|
config["cpLevel"],
|
||||||
|
config["cabinLevel"],
|
||||||
|
config["lv"],
|
||||||
|
]);
|
||||||
|
if (level.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return "Lv.$level";
|
||||||
|
}
|
||||||
|
|
||||||
|
String _firstRoomMicRelationConfigText(Iterable<dynamic> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.toString().trim() ?? "";
|
||||||
|
if (text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyHydratedRoomMicRelationProfile(
|
||||||
|
String roomId,
|
||||||
|
String userId,
|
||||||
|
SocialChatUserProfile hydratedProfile,
|
||||||
|
) {
|
||||||
|
final currentRoomId =
|
||||||
|
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||||
|
if (currentRoomId != roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var changed = false;
|
||||||
|
final mergedMap = roomWheatMap.map((index, mic) {
|
||||||
|
final user = mic.user;
|
||||||
|
if (user == null || _roomMicRelationUserId(user) != userId) {
|
||||||
|
return MapEntry(index, mic);
|
||||||
|
}
|
||||||
|
final mergedUser = _mergeRoomMicRelationProfile(user, hydratedProfile);
|
||||||
|
if (identical(mergedUser, user)) {
|
||||||
|
return MapEntry(index, mic);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return MapEntry(index, mic.copyWith(user: mergedUser));
|
||||||
|
});
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
roomWheatMap = mergedMap;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialChatUserProfile _mergeRoomMicRelationProfile(
|
||||||
|
SocialChatUserProfile user,
|
||||||
|
SocialChatUserProfile relationProfile,
|
||||||
|
) {
|
||||||
|
final nextCpList = relationProfile.cpList ?? const <CPRes>[];
|
||||||
|
final nextCloseFriendList =
|
||||||
|
relationProfile.closeFriendList ?? const <CPRes>[];
|
||||||
|
if (_sameRoomMicRelationLists(user.cpList, nextCpList) &&
|
||||||
|
_sameRoomMicRelationLists(user.closeFriendList, nextCloseFriendList)) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return user.copyWith(
|
||||||
|
cpList: nextCpList,
|
||||||
|
closeFriendList: nextCloseFriendList,
|
||||||
|
isCpRelation: nextCpList.isNotEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasCompleteRoomMicRelationLists(SocialChatUserProfile profile) {
|
||||||
|
return profile.cpList != null && profile.closeFriendList != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasRoomMicRelationPair(CPRes? pair) {
|
||||||
|
return (pair?.cpUserId?.trim().isNotEmpty ?? false) ||
|
||||||
|
(pair?.cpAccount?.trim().isNotEmpty ?? false) ||
|
||||||
|
(pair?.meUserId?.trim().isNotEmpty ?? false) ||
|
||||||
|
(pair?.meAccount?.trim().isNotEmpty ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _sameRoomMicRelationLists(List<CPRes>? left, List<CPRes>? right) {
|
||||||
|
final leftList = left ?? const <CPRes>[];
|
||||||
|
final rightList = right ?? const <CPRes>[];
|
||||||
|
if (leftList.length != rightList.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var index = 0; index < leftList.length; index += 1) {
|
||||||
|
if (_roomMicRelationPairKey(leftList[index]) !=
|
||||||
|
_roomMicRelationPairKey(rightList[index])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _roomMicRelationPairKey(CPRes pair) {
|
||||||
|
return [
|
||||||
|
pair.meUserId?.trim() ?? "",
|
||||||
|
pair.meAccount?.trim() ?? "",
|
||||||
|
pair.cpUserId?.trim() ?? "",
|
||||||
|
pair.cpAccount?.trim() ?? "",
|
||||||
|
pair.relationType?.trim() ?? "",
|
||||||
|
pair.status?.trim() ?? "",
|
||||||
|
].join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
String _roomMicRelationUserId(SocialChatUserProfile? user) {
|
||||||
|
return user?.id?.trim() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
bool _pruneRoomMusicPublishingUsersToMicSeats() {
|
bool _pruneRoomMusicPublishingUsersToMicSeats() {
|
||||||
if (_roomMusicPublishingUserIds.isEmpty) {
|
if (_roomMusicPublishingUserIds.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
@ -1519,6 +1793,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
await _roomRtcEngineAdapter?.leaveRoom();
|
await _roomRtcEngineAdapter?.leaveRoom();
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
if (clearMicSeats) {
|
if (clearMicSeats) {
|
||||||
|
_roomMicRelationCache.clear();
|
||||||
|
_roomMicRelationLoadingUserIds.clear();
|
||||||
roomWheatMap.clear();
|
roomWheatMap.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import 'package:yumi/shared/tools/sc_message_utils.dart';
|
|||||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_broadcast_assets.dart';
|
||||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||||
@ -293,6 +294,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
Timer? _roomRocketRewardPopupRetryTimer;
|
Timer? _roomRocketRewardPopupRetryTimer;
|
||||||
Timer? _roomRocketRoomReadyRewardRetryTimer;
|
Timer? _roomRocketRoomReadyRewardRetryTimer;
|
||||||
final Set<String> _shownCpInviteMessageKeys = <String>{};
|
final Set<String> _shownCpInviteMessageKeys = <String>{};
|
||||||
|
final Set<String> _forwardedCpInviteC2CMessageKeys = <String>{};
|
||||||
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
||||||
String? roomRedPacketBroadcastGroupId;
|
String? roomRedPacketBroadcastGroupId;
|
||||||
String? roomRedPacketBroadcastRegionCode;
|
String? roomRedPacketBroadcastRegionCode;
|
||||||
@ -886,6 +888,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
fallback: currentProfile.id,
|
fallback: currentProfile.id,
|
||||||
);
|
);
|
||||||
final relationType = scNormalizeCpRelationType(invite.relationType);
|
final relationType = scNormalizeCpRelationType(invite.relationType);
|
||||||
|
_forwardCpInviteC2CMessageIfNeeded(
|
||||||
|
sourceMessage: message,
|
||||||
|
data: data,
|
||||||
|
applyId: applyId,
|
||||||
|
invite: invite,
|
||||||
|
isSender: isSender,
|
||||||
|
);
|
||||||
if (isSender) {
|
if (isSender) {
|
||||||
final receiver = _cpInviteReceiverFromMessageData(data);
|
final receiver = _cpInviteReceiverFromMessageData(data);
|
||||||
_cpInviteLog(
|
_cpInviteLog(
|
||||||
@ -968,14 +977,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
|
|
||||||
SCSystemInvitMessageRes? _cpInviteFromMessageData(Map<String, dynamic> data) {
|
SCSystemInvitMessageRes? _cpInviteFromMessageData(Map<String, dynamic> data) {
|
||||||
try {
|
try {
|
||||||
final content = data["content"];
|
return SCSystemInvitMessageRes.fromMessageData(data);
|
||||||
if (content is String && content.trim().isNotEmpty) {
|
|
||||||
final decoded = jsonDecode(content);
|
|
||||||
return SCSystemInvitMessageRes.fromJson(decoded);
|
|
||||||
}
|
|
||||||
if (content is Map) {
|
|
||||||
return SCSystemInvitMessageRes.fromJson(content);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_cpInviteLog('parse CP_BUILD content failed error=$error data=$data');
|
_cpInviteLog('parse CP_BUILD content failed error=$error data=$data');
|
||||||
}
|
}
|
||||||
@ -1351,6 +1353,110 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _forwardCpInviteC2CMessageIfNeeded({
|
||||||
|
required V2TimMessage? sourceMessage,
|
||||||
|
required Map<String, dynamic> data,
|
||||||
|
required String applyId,
|
||||||
|
required SCSystemInvitMessageRes invite,
|
||||||
|
required bool isSender,
|
||||||
|
}) {
|
||||||
|
if (!isSender || sourceMessage?.isSelf == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final normalizedApplyId = applyId.trim();
|
||||||
|
if (normalizedApplyId.isEmpty ||
|
||||||
|
!_forwardedCpInviteC2CMessageKeys.add(normalizedApplyId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final receiverId = _cpInviteReceiverIdFromMessageData(data);
|
||||||
|
if (receiverId.isEmpty) {
|
||||||
|
_forwardedCpInviteC2CMessageKeys.remove(normalizedApplyId);
|
||||||
|
_cpInviteLog(
|
||||||
|
'skip forwarding CP_BUILD C2C reason=no_receiver '
|
||||||
|
'applyId=$normalizedApplyId',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cpInviteLog(
|
||||||
|
'forward backend CP_BUILD to C2C '
|
||||||
|
'receiver=$receiverId applyId=$normalizedApplyId',
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
sendCpInviteBuildMessage(
|
||||||
|
applyId: normalizedApplyId,
|
||||||
|
invite: invite,
|
||||||
|
receiverId: receiverId,
|
||||||
|
receiverProfile: _cpInviteReceiverProfileFromMessageData(data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _cpInviteReceiverIdFromMessageData(Map<String, dynamic> data) {
|
||||||
|
final content = _cpInviteContentMap(data);
|
||||||
|
return _firstCpInviteValue([
|
||||||
|
data["acceptActualAccount"]?.toString(),
|
||||||
|
data["acceptUserId"]?.toString(),
|
||||||
|
data["receiverActualAccount"]?.toString(),
|
||||||
|
data["receiverId"]?.toString(),
|
||||||
|
data["receiverUserId"]?.toString(),
|
||||||
|
data["targetUserId"]?.toString(),
|
||||||
|
data["toUserId"]?.toString(),
|
||||||
|
content["acceptActualAccount"]?.toString(),
|
||||||
|
content["acceptUserId"]?.toString(),
|
||||||
|
content["receiverActualAccount"]?.toString(),
|
||||||
|
content["receiverId"]?.toString(),
|
||||||
|
content["receiverUserId"]?.toString(),
|
||||||
|
content["targetUserId"]?.toString(),
|
||||||
|
content["toUserId"]?.toString(),
|
||||||
|
]) ??
|
||||||
|
"";
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialChatUserProfile? _cpInviteReceiverProfileFromMessageData(
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
|
final content = _cpInviteContentMap(data);
|
||||||
|
final id = _cpInviteReceiverIdFromMessageData(data);
|
||||||
|
final account = _firstCpInviteValue([
|
||||||
|
data["acceptAccount"]?.toString(),
|
||||||
|
data["receiverAccount"]?.toString(),
|
||||||
|
content["acceptAccount"]?.toString(),
|
||||||
|
content["receiverAccount"]?.toString(),
|
||||||
|
]);
|
||||||
|
final nickname = _firstCpInviteValue([
|
||||||
|
data["acceptNickname"]?.toString(),
|
||||||
|
data["acceptUserNickname"]?.toString(),
|
||||||
|
data["receiverNickname"]?.toString(),
|
||||||
|
data["receiverUserNickname"]?.toString(),
|
||||||
|
content["acceptNickname"]?.toString(),
|
||||||
|
content["acceptUserNickname"]?.toString(),
|
||||||
|
content["receiverNickname"]?.toString(),
|
||||||
|
content["receiverUserNickname"]?.toString(),
|
||||||
|
]);
|
||||||
|
final avatar = _firstCpInviteValue([
|
||||||
|
data["acceptUserAvatar"]?.toString(),
|
||||||
|
data["acceptAvatar"]?.toString(),
|
||||||
|
data["receiverUserAvatar"]?.toString(),
|
||||||
|
data["receiverAvatar"]?.toString(),
|
||||||
|
content["acceptUserAvatar"]?.toString(),
|
||||||
|
content["acceptAvatar"]?.toString(),
|
||||||
|
content["receiverUserAvatar"]?.toString(),
|
||||||
|
content["receiverAvatar"]?.toString(),
|
||||||
|
]);
|
||||||
|
if (id.isEmpty &&
|
||||||
|
(account ?? "").isEmpty &&
|
||||||
|
(nickname ?? "").isEmpty &&
|
||||||
|
(avatar ?? "").isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SocialChatUserProfile(
|
||||||
|
id: id.isNotEmpty ? id : null,
|
||||||
|
account: account,
|
||||||
|
userNickname: nickname,
|
||||||
|
userAvatar: avatar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> sendCpInviteBuildMessage({
|
Future<void> sendCpInviteBuildMessage({
|
||||||
required String applyId,
|
required String applyId,
|
||||||
required SCSystemInvitMessageRes invite,
|
required SCSystemInvitMessageRes invite,
|
||||||
@ -2270,6 +2376,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
roomAllMsgList.removeAt(roomAllMsgList.length - 1);
|
roomAllMsgList.removeAt(roomAllMsgList.length - 1);
|
||||||
}
|
}
|
||||||
msgAllListener?.call(msg);
|
msgAllListener?.call(msg);
|
||||||
|
_enqueueCpRelationBroadcastFloatingIfNeeded(msg);
|
||||||
_showCpRelationFormedDialogIfNeeded(msg);
|
_showCpRelationFormedDialogIfNeeded(msg);
|
||||||
|
|
||||||
if (msg.type == SCRoomMsgType.text) {
|
if (msg.type == SCRoomMsgType.text) {
|
||||||
@ -2327,6 +2434,71 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _enqueueCpRelationBroadcastFloatingIfNeeded(Msg msg) {
|
||||||
|
if (msg.type != SCRoomMsgType.cpSystemNotice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final action = msg.noticeAction?.trim() ?? "";
|
||||||
|
if (action != "RELATION_ESTABLISHED" && action != "RELATION_JOINED_ROOM") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_isLiveCpRelationBroadcastMessage(msg) ||
|
||||||
|
!_shouldShowCpRelationBroadcastFloating(msg.groupId ?? "")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final relationType = scNormalizeCpRelationType(msg.role);
|
||||||
|
final isEntry = action == "RELATION_JOINED_ROOM";
|
||||||
|
final firstProfile = isEntry ? msg.user : (msg.toUser ?? msg.user);
|
||||||
|
final secondProfile = isEntry ? msg.toUser : (msg.user ?? msg.toUser);
|
||||||
|
OverlayManager().addMessage(
|
||||||
|
SCFloatingMessage(
|
||||||
|
type: scCpRelationBroadcastFloatingType,
|
||||||
|
priority: 970,
|
||||||
|
roomId: msg.groupId ?? "",
|
||||||
|
userId: _roomMsgUserId(firstProfile),
|
||||||
|
toUserId: _roomMsgUserId(secondProfile),
|
||||||
|
userAvatarUrl: firstProfile?.userAvatar ?? "",
|
||||||
|
toUserAvatarUrl: secondProfile?.userAvatar ?? "",
|
||||||
|
userName: _roomMsgUserDisplayName(firstProfile),
|
||||||
|
toUserName: _roomMsgUserDisplayName(secondProfile),
|
||||||
|
giftUrl: scCpRelationBroadcastAsset(relationType),
|
||||||
|
giftId: action,
|
||||||
|
giftTab: relationType,
|
||||||
|
displayText: msg.msg ?? "",
|
||||||
|
durationSeconds: 5,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isLiveCpRelationBroadcastMessage(Msg msg) {
|
||||||
|
final timestamp = msg.time ?? 0;
|
||||||
|
if (timestamp <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return DateTime.now().millisecondsSinceEpoch - timestamp < 60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shouldShowCpRelationBroadcastFloating(String groupId) {
|
||||||
|
final normalizedGroupId = groupId.trim();
|
||||||
|
final currentContext = context;
|
||||||
|
if (normalizedGroupId.isEmpty ||
|
||||||
|
currentContext == null ||
|
||||||
|
!currentContext.mounted ||
|
||||||
|
!_isCurrentVoiceRoomGroup(normalizedGroupId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||||
|
currentContext,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
return rtcProvider.shouldShowRoomVisualEffects &&
|
||||||
|
rtcProvider.isVoiceRoomRouteVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _roomMsgUserId(SocialChatUserProfile? profile) {
|
||||||
|
return _firstCpInviteValue([profile?.id, profile?.account]) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
void _showCpRelationFormedDialogIfNeeded(Msg msg) {
|
void _showCpRelationFormedDialogIfNeeded(Msg msg) {
|
||||||
if (msg.type != SCRoomMsgType.cpSystemNotice ||
|
if (msg.type != SCRoomMsgType.cpSystemNotice ||
|
||||||
msg.noticeAction != "RELATION_ESTABLISHED") {
|
msg.noticeAction != "RELATION_ESTABLISHED") {
|
||||||
@ -2792,6 +2964,93 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
OverlayManager().addMessage(_buildLuckyGiftFloatingMessage(broadCastRes));
|
OverlayManager().addMessage(_buildLuckyGiftFloatingMessage(broadCastRes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleGameWinBroadcast(
|
||||||
|
dynamic rawData, {
|
||||||
|
required bool isRegionBroadcast,
|
||||||
|
}) {
|
||||||
|
if (SCGlobalConfig.isReview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final wrapper = _broadcastPayloadMap(rawData);
|
||||||
|
final payload = _broadcastPayloadMap(wrapper['data'] ?? rawData);
|
||||||
|
if (payload.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final winCoins =
|
||||||
|
_payloadNum(payload['currencyDiff']) ??
|
||||||
|
_payloadNum(payload['winAmount']) ??
|
||||||
|
_payloadNum(payload['awardAmount']) ??
|
||||||
|
0;
|
||||||
|
if (winCoins <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final roomId = _payloadRoomId(payload);
|
||||||
|
final userId = _firstNonBlank([
|
||||||
|
_payloadText(payload['account']),
|
||||||
|
_payloadText(payload['actualAccount']),
|
||||||
|
_payloadText(payload['userId']),
|
||||||
|
_payloadText(payload['sendUserId']),
|
||||||
|
_payloadText(payload['senderUserId']),
|
||||||
|
]);
|
||||||
|
final userName = _firstNonBlank([
|
||||||
|
_payloadText(payload['userNickname']),
|
||||||
|
_payloadText(payload['nickname']),
|
||||||
|
_payloadText(payload['userName']),
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
final gameId = _firstNonBlank([
|
||||||
|
_payloadText(payload['currentGameId']),
|
||||||
|
_payloadText(payload['gameId']),
|
||||||
|
_payloadText(payload['providerGameId']),
|
||||||
|
_payloadText(payload['vendorGameId']),
|
||||||
|
_payloadText(payload['currentProviderGameId']),
|
||||||
|
_payloadText(payload['currentVendorGameId']),
|
||||||
|
]);
|
||||||
|
final gameName = _firstNonBlank([
|
||||||
|
_payloadText(payload['currentGameName']),
|
||||||
|
_payloadText(payload['gameName']),
|
||||||
|
_payloadText(payload['name']),
|
||||||
|
'game',
|
||||||
|
]);
|
||||||
|
final gameIconUrl = _firstNonBlank([
|
||||||
|
_payloadAssetText(payload['gameUrl']),
|
||||||
|
_payloadAssetText(payload['currentGameCover']),
|
||||||
|
_payloadAssetText(payload['gameCover']),
|
||||||
|
_payloadAssetText(payload['cover']),
|
||||||
|
_payloadAssetText(payload['iconUrl']),
|
||||||
|
_payloadAssetText(payload['icon']),
|
||||||
|
]);
|
||||||
|
final roundId = _firstNonBlank([
|
||||||
|
_payloadText(payload['gameRoundId']),
|
||||||
|
_payloadText(payload['game_round_id']),
|
||||||
|
_payloadText(payload['roundId']),
|
||||||
|
_payloadText(payload['roundNo']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
OverlayManager().addMessage(
|
||||||
|
SCFloatingMessage(
|
||||||
|
type: 2,
|
||||||
|
userId: userId,
|
||||||
|
userAvatarUrl: _payloadAssetText(payload['userAvatar']),
|
||||||
|
userName: userName,
|
||||||
|
toUserName: gameName,
|
||||||
|
giftUrl: gameIconUrl,
|
||||||
|
giftId: roundId,
|
||||||
|
gameId: gameId,
|
||||||
|
roomId: roomId,
|
||||||
|
coins: winCoins,
|
||||||
|
multiple:
|
||||||
|
_payloadNum(payload['multiple']) ??
|
||||||
|
_payloadNum(payload['winMultiple']),
|
||||||
|
displayText: _payloadText(payload['msg']),
|
||||||
|
broadcastScope:
|
||||||
|
isRegionBroadcast ? SCFloatingMessage.broadcastScopeRegion : '',
|
||||||
|
priority: 1000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleRoomLuckyGiftMessage(SCBroadCastLuckGiftPush broadCastRes) {
|
void _handleRoomLuckyGiftMessage(SCBroadCastLuckGiftPush broadCastRes) {
|
||||||
final rewardData = broadCastRes.data;
|
final rewardData = broadCastRes.data;
|
||||||
if (rewardData == null) {
|
if (rewardData == null) {
|
||||||
@ -4653,25 +4912,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
} else if (type == "GAME_BAISHUN_WIN") {
|
} else if (type == "GAME_BAISHUN_WIN") {
|
||||||
if (SCGlobalConfig.isReview) {
|
_handleGameWinBroadcast(
|
||||||
///审核状态不播放动画
|
data,
|
||||||
return;
|
isRegionBroadcast: isRegionBroadcastGroup,
|
||||||
}
|
);
|
||||||
var fdata = data["data"];
|
|
||||||
var winCoins = fdata["currencyDiff"];
|
|
||||||
if (winCoins > 14999) {
|
|
||||||
///达到5000才飘屏
|
|
||||||
SCFloatingMessage msg = SCFloatingMessage(
|
|
||||||
type: 2,
|
|
||||||
userId: fdata["account"],
|
|
||||||
userAvatarUrl: fdata["userAvatar"],
|
|
||||||
userName: fdata["userNickname"],
|
|
||||||
giftUrl: fdata["gameUrl"],
|
|
||||||
roomId: fdata["roomId"],
|
|
||||||
coins: fdata["currencyDiff"],
|
|
||||||
);
|
|
||||||
OverlayManager().addMessage(msg);
|
|
||||||
}
|
|
||||||
} else if (type == "GAME_LUCKY_GIFT") {
|
} else if (type == "GAME_LUCKY_GIFT") {
|
||||||
final broadCastRes = SCBroadCastLuckGiftPush.fromJson(data);
|
final broadCastRes = SCBroadCastLuckGiftPush.fromJson(data);
|
||||||
_giftFxLog(
|
_giftFxLog(
|
||||||
@ -4890,6 +5134,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data["type"] == "GAME_BAISHUN_WIN") {
|
||||||
|
_handleGameWinBroadcast(data, isRegionBroadcast: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Msg msg = Msg.fromJson(data);
|
Msg msg = Msg.fromJson(data);
|
||||||
|
|
||||||
if (msg.type == SCRoomMsgType.sendGift ||
|
if (msg.type == SCRoomMsgType.sendGift ||
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
// account : ""
|
// account : ""
|
||||||
// countryCode : ""
|
// countryCode : ""
|
||||||
// countryName : ""
|
// countryName : ""
|
||||||
@ -62,7 +64,7 @@ class SCSystemInvitMessageRes {
|
|||||||
_actualAccount = json['actualAccount'];
|
_actualAccount = json['actualAccount'];
|
||||||
_applyType = json['applyType'];
|
_applyType = json['applyType'];
|
||||||
_content = json['content'];
|
_content = json['content'];
|
||||||
_relationType = json['relationType'];
|
_relationType = _firstNonBlank(json, _relationTypeKeys);
|
||||||
_dismissEndTime = _asInt(json['dismissEndTime']);
|
_dismissEndTime = _asInt(json['dismissEndTime']);
|
||||||
_giftCover = json['giftCover'];
|
_giftCover = json['giftCover'];
|
||||||
_giftCount = _asInt(json['giftCount']);
|
_giftCount = _asInt(json['giftCount']);
|
||||||
@ -126,8 +128,43 @@ class SCSystemInvitMessageRes {
|
|||||||
map['giftCount'] = _giftCount;
|
map['giftCount'] = _giftCount;
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SCSystemInvitMessageRes? fromMessageData(dynamic data) {
|
||||||
|
final root = _stringKeyMap(data);
|
||||||
|
if (root.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final content = _contentMap(root['content']);
|
||||||
|
if (content.isEmpty &&
|
||||||
|
root.containsKey('content') &&
|
||||||
|
!_hasInviteIdentity(root)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final source = <String, dynamic>{...(content.isNotEmpty ? content : root)};
|
||||||
|
final relationType =
|
||||||
|
_firstNonBlank(source, _relationTypeKeys) ??
|
||||||
|
_firstNonBlank(root, _relationTypeKeys) ??
|
||||||
|
_relationTypeFromGiftPayload(source) ??
|
||||||
|
_relationTypeFromGiftPayload(root);
|
||||||
|
if (relationType != null && relationType.isNotEmpty) {
|
||||||
|
source['relationType'] = relationType;
|
||||||
|
}
|
||||||
|
return SCSystemInvitMessageRes.fromJson(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const List<String> _relationTypeKeys = [
|
||||||
|
'relationType',
|
||||||
|
'relation_type',
|
||||||
|
'cpRelationType',
|
||||||
|
'cp_relation_type',
|
||||||
|
'relationshipType',
|
||||||
|
'relationship_type',
|
||||||
|
'relation',
|
||||||
|
'cpType',
|
||||||
|
'cp_type',
|
||||||
|
];
|
||||||
|
|
||||||
String? _firstNonBlank(dynamic json, List<String> keys) {
|
String? _firstNonBlank(dynamic json, List<String> keys) {
|
||||||
if (json is! Map) {
|
if (json is! Map) {
|
||||||
return null;
|
return null;
|
||||||
@ -140,3 +177,106 @@ String? _firstNonBlank(dynamic json, List<String> keys) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _hasInviteIdentity(Map<String, dynamic> source) {
|
||||||
|
return _firstNonBlank(source, const [
|
||||||
|
'account',
|
||||||
|
'actualAccount',
|
||||||
|
'userAvatar',
|
||||||
|
'userNickname',
|
||||||
|
'nickname',
|
||||||
|
]) !=
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _stringKeyMap(dynamic value) {
|
||||||
|
if (value is Map<String, dynamic>) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is Map) {
|
||||||
|
return value.map((key, item) => MapEntry(key.toString(), item));
|
||||||
|
}
|
||||||
|
return const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _contentMap(dynamic content) {
|
||||||
|
if (content is String && content.trim().isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(content);
|
||||||
|
return _stringKeyMap(decoded);
|
||||||
|
} catch (_) {
|
||||||
|
return const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _stringKeyMap(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _relationTypeFromGiftPayload(Map<String, dynamic> source) {
|
||||||
|
for (final key in const ['gift', 'giftInfo', 'giftConfig', 'giftVO']) {
|
||||||
|
final gift = _stringKeyMap(source[key]);
|
||||||
|
if (gift.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final explicit = _firstNonBlank(gift, _relationTypeKeys);
|
||||||
|
if (explicit != null && explicit.isNotEmpty) {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
|
final inferred = _inferRelationTypeFromGift(gift);
|
||||||
|
if (inferred != null) {
|
||||||
|
return inferred;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _inferRelationTypeFromGift(Map<String, dynamic> gift) {
|
||||||
|
for (final key in const [
|
||||||
|
'giftCode',
|
||||||
|
'giftName',
|
||||||
|
'name',
|
||||||
|
'special',
|
||||||
|
'standardId',
|
||||||
|
]) {
|
||||||
|
final inferred = _inferRelationTypeFromText(gift[key]?.toString());
|
||||||
|
if (inferred != null) {
|
||||||
|
return inferred;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _inferRelationTypeFromText(String? value) {
|
||||||
|
final upper = value?.trim().toUpperCase();
|
||||||
|
if (upper == null || upper.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (_hasRelationToken(upper, const ['SIS', 'SISTER', 'SISTERS'])) {
|
||||||
|
return 'SISTERS';
|
||||||
|
}
|
||||||
|
if (_hasRelationToken(upper, const [
|
||||||
|
'BRO',
|
||||||
|
'BROS',
|
||||||
|
'BROTHER',
|
||||||
|
'BROTHERS',
|
||||||
|
'SWORN_BRO',
|
||||||
|
'SWORN_BROS',
|
||||||
|
'SWORN_BROTHER',
|
||||||
|
'SWORN_BROTHERS',
|
||||||
|
])) {
|
||||||
|
return 'BROTHER';
|
||||||
|
}
|
||||||
|
if (_hasRelationToken(upper, const ['CP', 'COUPLE'])) {
|
||||||
|
return 'CP';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasRelationToken(String upperText, List<String> tokens) {
|
||||||
|
for (final token in tokens) {
|
||||||
|
final escaped = RegExp.escape(token);
|
||||||
|
if (RegExp('(^|[^A-Z0-9])$escaped([^A-Z0-9]|\$)').hasMatch(upperText)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@ class SCFloatingMessage {
|
|||||||
String? fallbackUrl; // 兜底资源
|
String? fallbackUrl; // 兜底资源
|
||||||
String? giftId; // 礼物id
|
String? giftId; // 礼物id
|
||||||
String? giftTab; // 礼物分类
|
String? giftTab; // 礼物分类
|
||||||
|
String? gameId; // 游戏id
|
||||||
|
String? displayText; // 飘窗展示文案
|
||||||
int? type;
|
int? type;
|
||||||
int? rocketLevel;
|
int? rocketLevel;
|
||||||
num? coins;
|
num? coins;
|
||||||
@ -35,6 +37,8 @@ class SCFloatingMessage {
|
|||||||
this.fallbackUrl = '',
|
this.fallbackUrl = '',
|
||||||
this.giftId = '',
|
this.giftId = '',
|
||||||
this.giftTab = '',
|
this.giftTab = '',
|
||||||
|
this.gameId = '',
|
||||||
|
this.displayText = '',
|
||||||
this.number = 0,
|
this.number = 0,
|
||||||
this.coins = 0,
|
this.coins = 0,
|
||||||
this.durationSeconds,
|
this.durationSeconds,
|
||||||
@ -57,6 +61,8 @@ class SCFloatingMessage {
|
|||||||
fallbackUrl = json['fallbackUrl'];
|
fallbackUrl = json['fallbackUrl'];
|
||||||
giftId = json['giftId'];
|
giftId = json['giftId'];
|
||||||
giftTab = json['giftTab'];
|
giftTab = json['giftTab'];
|
||||||
|
gameId = json['gameId']?.toString();
|
||||||
|
displayText = json['displayText']?.toString();
|
||||||
coins = json['coins'];
|
coins = json['coins'];
|
||||||
number = json['number'];
|
number = json['number'];
|
||||||
durationSeconds = json['durationSeconds'];
|
durationSeconds = json['durationSeconds'];
|
||||||
@ -82,6 +88,8 @@ class SCFloatingMessage {
|
|||||||
map['fallbackUrl'] = fallbackUrl;
|
map['fallbackUrl'] = fallbackUrl;
|
||||||
map['giftId'] = giftId;
|
map['giftId'] = giftId;
|
||||||
map['giftTab'] = giftTab;
|
map['giftTab'] = giftTab;
|
||||||
|
map['gameId'] = gameId;
|
||||||
|
map['displayText'] = displayText;
|
||||||
map['coins'] = coins;
|
map['coins'] = coins;
|
||||||
map['number'] = number;
|
map['number'] = number;
|
||||||
map['durationSeconds'] = durationSeconds;
|
map['durationSeconds'] = durationSeconds;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widg
|
|||||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart';
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_vip_entry_screen_widget.dart';
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_vip_entry_screen_widget.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_cp_relation_screen_widget.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
|
|
||||||
@ -30,8 +31,9 @@ class OverlayManager {
|
|||||||
static const Duration _rocketFloatingFallbackDedupWindow = Duration(
|
static const Duration _rocketFloatingFallbackDedupWindow = Duration(
|
||||||
seconds: 30,
|
seconds: 30,
|
||||||
);
|
);
|
||||||
|
static const Duration _gameWinFloatingDedupWindow = Duration(seconds: 10);
|
||||||
static const int _maxStackedFloatingVisibleCount = 2;
|
static const int _maxStackedFloatingVisibleCount = 2;
|
||||||
static const List<int> _stackedFloatingOrder = [5, 1, 4, 3];
|
static const List<int> _stackedFloatingOrder = [6, 5, 1, 4, 3];
|
||||||
static const double _stackedFloatingBaseTop = 70;
|
static const double _stackedFloatingBaseTop = 70;
|
||||||
static const double _stackedFloatingGap = 8;
|
static const double _stackedFloatingGap = 8;
|
||||||
|
|
||||||
@ -190,10 +192,45 @@ class OverlayManager {
|
|||||||
}
|
}
|
||||||
return 'room_red_packet|$packetId';
|
return 'room_red_packet|$packetId';
|
||||||
}
|
}
|
||||||
|
if (message.type == 2) {
|
||||||
|
final roundId = message.giftId?.trim() ?? '';
|
||||||
|
final userId = message.userId?.trim() ?? '';
|
||||||
|
if (roundId.isNotEmpty) {
|
||||||
|
return 'game_win|$roundId|$userId';
|
||||||
|
}
|
||||||
|
final roomId = message.roomId?.trim() ?? '';
|
||||||
|
final gameId = message.gameId?.trim() ?? '';
|
||||||
|
final coins = message.coins ?? 0;
|
||||||
|
if (roomId.isEmpty && userId.isEmpty && gameId.isEmpty && coins == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 'game_win_fallback|$roomId|$userId|$gameId|$coins';
|
||||||
|
}
|
||||||
|
if (message.type == 6) {
|
||||||
|
final roomId = message.roomId?.trim() ?? '';
|
||||||
|
final action = message.giftId?.trim() ?? '';
|
||||||
|
final relationType = message.giftTab?.trim() ?? '';
|
||||||
|
final userId = message.userId?.trim() ?? '';
|
||||||
|
final toUserId = message.toUserId?.trim() ?? '';
|
||||||
|
if (roomId.isEmpty &&
|
||||||
|
action.isEmpty &&
|
||||||
|
relationType.isEmpty &&
|
||||||
|
userId.isEmpty &&
|
||||||
|
toUserId.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 'cp_relation|$roomId|$action|$relationType|$userId|$toUserId';
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration _floatingDedupWindowFor(SCFloatingMessage message) {
|
Duration _floatingDedupWindowFor(SCFloatingMessage message) {
|
||||||
|
if (message.type == 2) {
|
||||||
|
return _gameWinFloatingDedupWindow;
|
||||||
|
}
|
||||||
|
if (message.type == 6) {
|
||||||
|
return _gameWinFloatingDedupWindow;
|
||||||
|
}
|
||||||
if (message.type == 3 && (message.giftId?.trim() ?? '').isEmpty) {
|
if (message.type == 3 && (message.giftId?.trim() ?? '').isEmpty) {
|
||||||
return _rocketFloatingFallbackDedupWindow;
|
return _rocketFloatingFallbackDedupWindow;
|
||||||
}
|
}
|
||||||
@ -245,13 +282,16 @@ class OverlayManager {
|
|||||||
unawaited(
|
unawaited(
|
||||||
warmImageResource(
|
warmImageResource(
|
||||||
message.giftUrl ?? "",
|
message.giftUrl ?? "",
|
||||||
logicalWidth: message.type == 5 ? 350 : 96,
|
logicalWidth: message.type == 5 || message.type == 6 ? 350 : 96,
|
||||||
logicalHeight: message.type == 5 ? 84 : 96,
|
logicalHeight: message.type == 5 || message.type == 6 ? 84 : 96,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (message.type == 0) {
|
if (message.type == 0) {
|
||||||
unawaited(_warmLuckyGiftBannerSvga());
|
unawaited(_warmLuckyGiftBannerSvga());
|
||||||
}
|
}
|
||||||
|
if (message.type == 2) {
|
||||||
|
unawaited(_warmGameWinBannerSvga());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _warmLuckyGiftBannerSvga() async {
|
Future<void> _warmLuckyGiftBannerSvga() async {
|
||||||
@ -264,6 +304,16 @@ class OverlayManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _warmGameWinBannerSvga() async {
|
||||||
|
try {
|
||||||
|
await SCSvgaAssetWidget.preloadAsset(
|
||||||
|
FloatingGameScreenWidget.backgroundSvgaAssetPath,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Preload failure is non-fatal; the banner will retry during rendering.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _safeScheduleNext() {
|
void _safeScheduleNext() {
|
||||||
if (_isSuppressed) return;
|
if (_isSuppressed) return;
|
||||||
if (_isProcessing || _messageQueue.isEmpty) return;
|
if (_isProcessing || _messageQueue.isEmpty) return;
|
||||||
@ -470,6 +520,12 @@ class OverlayManager {
|
|||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
case 6:
|
||||||
|
//CP / 挚友关系飘窗
|
||||||
|
return FloatingCpRelationScreenWidget(
|
||||||
|
message: message,
|
||||||
|
onAnimationCompleted: onComplete,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
onComplete();
|
onComplete();
|
||||||
return Container();
|
return Container();
|
||||||
@ -487,6 +543,8 @@ class OverlayManager {
|
|||||||
return 4;
|
return 4;
|
||||||
case 5:
|
case 5:
|
||||||
return 5;
|
return 5;
|
||||||
|
case 6:
|
||||||
|
return 6;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -528,6 +586,8 @@ class OverlayManager {
|
|||||||
return 66;
|
return 66;
|
||||||
case 5:
|
case 5:
|
||||||
return 84;
|
return 84;
|
||||||
|
case 6:
|
||||||
|
return FloatingCpRelationScreenWidget.bannerHeight;
|
||||||
default:
|
default:
|
||||||
return 68;
|
return 68;
|
||||||
}
|
}
|
||||||
@ -679,6 +739,9 @@ class OverlayManager {
|
|||||||
if (normalizedRoomId.isNotEmpty) {
|
if (normalizedRoomId.isNotEmpty) {
|
||||||
_removeMessagesByType(3, roomId: normalizedRoomId);
|
_removeMessagesByType(3, roomId: normalizedRoomId);
|
||||||
}
|
}
|
||||||
|
if (normalizedRoomId.isNotEmpty) {
|
||||||
|
_removeMessagesByType(2, roomId: normalizedRoomId);
|
||||||
|
}
|
||||||
if (normalizedRoomId.isNotEmpty) {
|
if (normalizedRoomId.isNotEmpty) {
|
||||||
_removeMessagesByType(4, roomId: normalizedRoomId);
|
_removeMessagesByType(4, roomId: normalizedRoomId);
|
||||||
}
|
}
|
||||||
@ -734,9 +797,14 @@ class OverlayManager {
|
|||||||
return message.type == 3 && message.isRegionBroadcast;
|
return message.type == 3 && message.isRegionBroadcast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isRegionGameWinMessage(SCFloatingMessage message) {
|
||||||
|
return message.type == 2 && message.isRegionBroadcast;
|
||||||
|
}
|
||||||
|
|
||||||
bool _isCrossRoomRegionFloatingMessage(SCFloatingMessage message) {
|
bool _isCrossRoomRegionFloatingMessage(SCFloatingMessage message) {
|
||||||
return _isRegionRedPacketMessage(message) ||
|
return _isRegionRedPacketMessage(message) ||
|
||||||
_isRegionRoomRocketMessage(message);
|
_isRegionRoomRocketMessage(message) ||
|
||||||
|
_isRegionGameWinMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _shouldDeferRegionRedPacketMessage(
|
bool _shouldDeferRegionRedPacketMessage(
|
||||||
@ -856,15 +924,25 @@ class OverlayManager {
|
|||||||
currentRoomId == messageRoomId;
|
currentRoomId == messageRoomId;
|
||||||
if (isCurrentVisibleRoom) {
|
if (isCurrentVisibleRoom) {
|
||||||
// 区域红包/火箭在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。
|
// 区域红包/火箭在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。
|
||||||
return message.type == 4 || message.type == 3;
|
return message.type == 4 || message.type == 3 || message.type == 2;
|
||||||
}
|
}
|
||||||
final isCurrentRoomMessage =
|
final isCurrentRoomMessage =
|
||||||
currentRoomId.isNotEmpty &&
|
currentRoomId.isNotEmpty &&
|
||||||
messageRoomId.isNotEmpty &&
|
messageRoomId.isNotEmpty &&
|
||||||
currentRoomId == messageRoomId;
|
currentRoomId == messageRoomId;
|
||||||
if ((message.type == 4 || message.type == 3) && isCurrentRoomMessage) {
|
if ((message.type == 4 || message.type == 3 || message.type == 2) &&
|
||||||
|
isCurrentRoomMessage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.type == 2) {
|
||||||
|
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rtcProvider.shouldShowRoomVisualEffects) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
||||||
|
}
|
||||||
if ((message.type == 4 || message.type == 3) &&
|
if ((message.type == 4 || message.type == 3) &&
|
||||||
rtcProvider.shouldShowRoomVisualEffects) {
|
rtcProvider.shouldShowRoomVisualEffects) {
|
||||||
return true;
|
return true;
|
||||||
@ -920,6 +998,7 @@ class OverlayManager {
|
|||||||
if (activeMessage != null &&
|
if (activeMessage != null &&
|
||||||
(activeMessage.type == 0 ||
|
(activeMessage.type == 0 ||
|
||||||
activeMessage.type == 1 ||
|
activeMessage.type == 1 ||
|
||||||
|
activeMessage.type == 2 ||
|
||||||
activeMessage.type == 3 ||
|
activeMessage.type == 3 ||
|
||||||
activeMessage.type == 5 ||
|
activeMessage.type == 5 ||
|
||||||
(includeRedPacket && activeMessage.type == 4)) &&
|
(includeRedPacket && activeMessage.type == 4)) &&
|
||||||
@ -959,6 +1038,7 @@ class OverlayManager {
|
|||||||
final isRoomFloatingType =
|
final isRoomFloatingType =
|
||||||
type == 0 ||
|
type == 0 ||
|
||||||
type == 1 ||
|
type == 1 ||
|
||||||
|
type == 2 ||
|
||||||
type == 3 ||
|
type == 3 ||
|
||||||
type == 5 ||
|
type == 5 ||
|
||||||
(includeRedPacket && type == 4);
|
(includeRedPacket && type == 4);
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_re
|
|||||||
import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/repositories/config_repository.dart';
|
import 'package:yumi/shared/business_logic/repositories/config_repository.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'
|
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'
|
||||||
show BaseNetworkClient;
|
show BaseNetworkClient, DioException;
|
||||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||||
import 'package:yumi/shared/tools/sc_deviceId_utils.dart';
|
import 'package:yumi/shared/tools/sc_deviceId_utils.dart';
|
||||||
|
|
||||||
@ -58,23 +58,37 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
///go/app/popups/entry
|
///app/popups/entry
|
||||||
@override
|
@override
|
||||||
Future<SCEntryPopupRes> entryPopup() async {
|
Future<SCEntryPopupRes> entryPopup() async {
|
||||||
final deviceId = await SCDeviceIdUtils.getDeviceId();
|
final deviceId = await SCDeviceIdUtils.getDeviceId();
|
||||||
final result = await http.post<SCEntryPopupRes>(
|
final data = {
|
||||||
"/go/app/popups/entry",
|
"scene": "APP_ENTER",
|
||||||
data: {
|
"platform": Platform.isIOS ? "ios" : "android",
|
||||||
"scene": "APP_ENTER",
|
"appVersion": SCGlobalConfig.version,
|
||||||
"platform": Platform.isIOS ? "ios" : "android",
|
"deviceId": deviceId,
|
||||||
"appVersion": SCGlobalConfig.version,
|
"language": SCGlobalConfig.lang,
|
||||||
"deviceId": deviceId,
|
};
|
||||||
"language": SCGlobalConfig.lang,
|
try {
|
||||||
},
|
return await _entryPopupByPath("/app/popups/entry", data);
|
||||||
|
} on DioException catch (error) {
|
||||||
|
if (error.response?.statusCode != 404) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
return _entryPopupByPath("/go/app/popups/entry", data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SCEntryPopupRes> _entryPopupByPath(
|
||||||
|
String path,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
|
return http.post<SCEntryPopupRes>(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||||
fromJson: (json) => SCEntryPopupRes.fromJson(json),
|
fromJson: (json) => SCEntryPopupRes.fromJson(json),
|
||||||
);
|
);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///sys/config/enum/config
|
///sys/config/enum/config
|
||||||
|
|||||||
15
lib/shared/tools/sc_cp_relation_broadcast_assets.dart
Normal file
15
lib/shared/tools/sc_cp_relation_broadcast_assets.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
|
||||||
|
const int scCpRelationBroadcastFloatingType = 6;
|
||||||
|
|
||||||
|
String scCpRelationBroadcastAsset(String? relationType) {
|
||||||
|
switch (scNormalizeCpRelationType(relationType)) {
|
||||||
|
case "BROTHER":
|
||||||
|
return "sc_images/room/cp_broadcast/brother_broadcast.pag";
|
||||||
|
case "SISTERS":
|
||||||
|
return "sc_images/room/cp_broadcast/sisters_broadcast.pag";
|
||||||
|
case "CP":
|
||||||
|
default:
|
||||||
|
return "sc_images/room/cp_broadcast/CP_broadcast.pag";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,6 +41,17 @@ String scCpRelationPossessiveLabel(String? relationType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String scCpRelationEntryLabel(String? relationType) {
|
||||||
|
switch (scNormalizeCpRelationType(relationType)) {
|
||||||
|
case "BROTHER":
|
||||||
|
return "brother";
|
||||||
|
case "SISTERS":
|
||||||
|
return "sister";
|
||||||
|
default:
|
||||||
|
return "cp";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String scCpRelationDialogTitle(String? relationType) {
|
String scCpRelationDialogTitle(String? relationType) {
|
||||||
switch (scNormalizeCpRelationType(relationType)) {
|
switch (scNormalizeCpRelationType(relationType)) {
|
||||||
case "BROTHER":
|
case "BROTHER":
|
||||||
@ -52,6 +63,19 @@ String scCpRelationDialogTitle(String? relationType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String scCpRelationDisplayDays(String? days) {
|
||||||
|
final text = days?.trim() ?? "";
|
||||||
|
final value = num.tryParse(text.replaceAll(",", ""));
|
||||||
|
if (value == null) {
|
||||||
|
return text.isEmpty ? "1" : text;
|
||||||
|
}
|
||||||
|
final displayValue = value + 1;
|
||||||
|
if (displayValue % 1 == 0) {
|
||||||
|
return displayValue.toInt().toString();
|
||||||
|
}
|
||||||
|
return displayValue.toString();
|
||||||
|
}
|
||||||
|
|
||||||
String scBuildCpRelationEstablishedNotice({
|
String scBuildCpRelationEstablishedNotice({
|
||||||
required String userName1,
|
required String userName1,
|
||||||
required String userName2,
|
required String userName2,
|
||||||
@ -66,7 +90,14 @@ String scBuildCpRelationJoinNotice({
|
|||||||
required String joinedName,
|
required String joinedName,
|
||||||
required String? relationType,
|
required String? relationType,
|
||||||
}) {
|
}) {
|
||||||
return "[System Notification] $ownerName's "
|
return "[System Notification] ${scBuildCpRelationEntryBroadcastText(ownerName: ownerName, joinedName: joinedName, relationType: relationType)}.";
|
||||||
"${scCpRelationPossessiveLabel(relationType)}, $joinedName, "
|
}
|
||||||
"has joined the party.";
|
|
||||||
|
String scBuildCpRelationEntryBroadcastText({
|
||||||
|
required String ownerName,
|
||||||
|
required String joinedName,
|
||||||
|
required String? relationType,
|
||||||
|
}) {
|
||||||
|
return "$ownerName's ${scCpRelationEntryLabel(relationType)} "
|
||||||
|
"$joinedName enter the room";
|
||||||
}
|
}
|
||||||
|
|||||||
130
lib/shared/tools/sc_cp_relation_pair_utils.dart
Normal file
130
lib/shared/tools/sc_cp_relation_pair_utils.dart
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
|
||||||
|
class SCCpRelationPairInfo {
|
||||||
|
const SCCpRelationPairInfo({required this.relationType, this.cpLevel});
|
||||||
|
|
||||||
|
final String relationType;
|
||||||
|
final int? cpLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? scCpRelationTypeBetweenProfiles(
|
||||||
|
SocialChatUserProfile? left,
|
||||||
|
SocialChatUserProfile? right,
|
||||||
|
) {
|
||||||
|
return scCpRelationInfoBetweenProfiles(left, right)?.relationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
SCCpRelationPairInfo? scCpRelationInfoBetweenProfiles(
|
||||||
|
SocialChatUserProfile? left,
|
||||||
|
SocialChatUserProfile? right,
|
||||||
|
) {
|
||||||
|
if (!_hasRelationIdentity(left) || !_hasRelationIdentity(right)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SCCpRelationPairInfo? fallbackInfo;
|
||||||
|
for (final relation in _relationsForProfile(left)) {
|
||||||
|
final relationInfo = _relationInfoIfContainsBoth(relation, left!, right!);
|
||||||
|
if (relationInfo != null) {
|
||||||
|
if (relationInfo.relationType != "CP" || relationInfo.cpLevel != null) {
|
||||||
|
return relationInfo;
|
||||||
|
}
|
||||||
|
fallbackInfo ??= relationInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final relation in _relationsForProfile(right)) {
|
||||||
|
final relationInfo = _relationInfoIfContainsBoth(relation, left!, right!);
|
||||||
|
if (relationInfo != null) {
|
||||||
|
if (relationInfo.relationType != "CP" || relationInfo.cpLevel != null) {
|
||||||
|
return relationInfo;
|
||||||
|
}
|
||||||
|
fallbackInfo ??= relationInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallbackInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
int scCpProfileRelationVersion(SocialChatUserProfile? profile) {
|
||||||
|
if (profile == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Object.hashAll(
|
||||||
|
_relationsForProfile(profile).map(
|
||||||
|
(relation) => Object.hash(
|
||||||
|
relation.meUserId ?? "",
|
||||||
|
relation.meAccount ?? "",
|
||||||
|
relation.cpUserId ?? "",
|
||||||
|
relation.cpAccount ?? "",
|
||||||
|
relation.relationType ?? "",
|
||||||
|
relation.status ?? "",
|
||||||
|
relation.levelText ?? "",
|
||||||
|
relation.cpValue ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<CPRes> _relationsForProfile(SocialChatUserProfile? profile) sync* {
|
||||||
|
yield* profile?.cpList ?? const <CPRes>[];
|
||||||
|
yield* profile?.closeFriendList ?? const <CPRes>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
SCCpRelationPairInfo? _relationInfoIfContainsBoth(
|
||||||
|
CPRes relation,
|
||||||
|
SocialChatUserProfile left,
|
||||||
|
SocialChatUserProfile right,
|
||||||
|
) {
|
||||||
|
final leftKeys = _profileKeys(left);
|
||||||
|
final rightKeys = _profileKeys(right);
|
||||||
|
if (leftKeys.isEmpty || rightKeys.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final relationKeys = _relationKeys(relation);
|
||||||
|
if (!relationKeys.any(leftKeys.contains) ||
|
||||||
|
!relationKeys.any(rightKeys.contains)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final relationType = scNormalizeCpRelationType(relation.relationType);
|
||||||
|
return SCCpRelationPairInfo(
|
||||||
|
relationType: relationType,
|
||||||
|
cpLevel: relationType == "CP" ? _cpLevelFromRelation(relation) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasRelationIdentity(SocialChatUserProfile? profile) =>
|
||||||
|
_profileKeys(profile).isNotEmpty;
|
||||||
|
|
||||||
|
Set<String> _profileKeys(SocialChatUserProfile? profile) {
|
||||||
|
return <String>{
|
||||||
|
_normalizedRelationKey(profile?.id),
|
||||||
|
_normalizedRelationKey(profile?.account),
|
||||||
|
}..remove("");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> _relationKeys(CPRes relation) {
|
||||||
|
return <String>{
|
||||||
|
_normalizedRelationKey(relation.meUserId),
|
||||||
|
_normalizedRelationKey(relation.meAccount),
|
||||||
|
_normalizedRelationKey(relation.cpUserId),
|
||||||
|
_normalizedRelationKey(relation.cpAccount),
|
||||||
|
}..remove("");
|
||||||
|
}
|
||||||
|
|
||||||
|
String _normalizedRelationKey(String? value) => value?.trim() ?? "";
|
||||||
|
|
||||||
|
int? _cpLevelFromRelation(CPRes relation) {
|
||||||
|
final levelText = relation.levelText?.trim() ?? "";
|
||||||
|
if (levelText.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final levelMatch = RegExp(
|
||||||
|
r'(?:lv\.?|level|等级|级)\s*([1-5])|([1-5])\s*(?:级|level)',
|
||||||
|
caseSensitive: false,
|
||||||
|
).firstMatch(levelText);
|
||||||
|
final rawLevel = levelMatch?.group(1) ?? levelMatch?.group(2);
|
||||||
|
final level = int.tryParse(rawLevel ?? "");
|
||||||
|
if (level == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return level.clamp(1, 5);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -68,43 +69,53 @@ class SCEntryPopupCoordinator {
|
|||||||
'response requestId=${response.requestId} '
|
'response requestId=${response.requestId} '
|
||||||
'queue=${response.queue} popups=${response.popups.keys.toList()}',
|
'queue=${response.queue} popups=${response.popups.keys.toList()}',
|
||||||
);
|
);
|
||||||
final popup = _pickDisplayablePopup(response);
|
final candidates = _displayablePopups(response);
|
||||||
if (popup == null) {
|
if (candidates.isEmpty) {
|
||||||
_log('skip reason=no-displayable-popup');
|
_log('skip reason=no-displayable-popup');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final imageUrl = normalizeImageResourceUrl(popup.image);
|
for (final popup in candidates) {
|
||||||
final imageReady = await warmImageResource(
|
final imageUrl = normalizeImageResourceUrl(popup.image);
|
||||||
imageUrl,
|
final imageReady = await warmImageResource(
|
||||||
logicalWidth: 326,
|
imageUrl,
|
||||||
timeout: const Duration(seconds: 8),
|
logicalWidth: 326,
|
||||||
);
|
timeout: const Duration(seconds: 8),
|
||||||
if (!imageReady || !context.mounted) {
|
);
|
||||||
_log(
|
if (!context.mounted) {
|
||||||
'skip reason=image-not-ready key=${popup.key} '
|
_log('skip reason=context-unmounted-after-image key=${popup.key}');
|
||||||
'image=${_clip(imageUrl)} mounted=${context.mounted}',
|
return;
|
||||||
|
}
|
||||||
|
if (!imageReady) {
|
||||||
|
_log(
|
||||||
|
'skip candidate key=${popup.key} reason=image-not-ready '
|
||||||
|
'image=${_clip(imageUrl)}',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_log('show key=${popup.key} popupId=${popup.popupId}');
|
||||||
|
await SCEntryPopupDialog.show(
|
||||||
|
context,
|
||||||
|
popup: popup,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
onDisplayed: () {
|
||||||
|
unawaited(_markShown(popup));
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
unawaited(_handlePopupTap(context, popup, onSelectRootTab));
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('show key=${popup.key} popupId=${popup.popupId}');
|
_log('skip reason=no-image-ready-candidates');
|
||||||
await SCEntryPopupDialog.show(
|
|
||||||
context,
|
|
||||||
popup: popup,
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
onDisplayed: () {
|
|
||||||
unawaited(_markShown(popup));
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
unawaited(_handlePopupTap(context, popup, onSelectRootTab));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
_log('request failed error=$error');
|
||||||
} finally {
|
} finally {
|
||||||
_isRunning = false;
|
_isRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static SCEntryPopupItem? _pickDisplayablePopup(SCEntryPopupRes response) {
|
static List<SCEntryPopupItem> _displayablePopups(SCEntryPopupRes response) {
|
||||||
|
final candidates = <SCEntryPopupItem>[];
|
||||||
final queuedKeys = <String>{};
|
final queuedKeys = <String>{};
|
||||||
for (final popupKey in response.queue) {
|
for (final popupKey in response.queue) {
|
||||||
queuedKeys.add(popupKey);
|
queuedKeys.add(popupKey);
|
||||||
@ -119,7 +130,7 @@ class SCEntryPopupCoordinator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_log('candidate key=${popup.key} source=queue');
|
_log('candidate key=${popup.key} source=queue');
|
||||||
return popup;
|
candidates.add(popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
final fallbackPopups =
|
final fallbackPopups =
|
||||||
@ -134,9 +145,9 @@ class SCEntryPopupCoordinator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_log('candidate key=${popup.key} source=priority-fallback');
|
_log('candidate key=${popup.key} source=priority-fallback');
|
||||||
return popup;
|
candidates.add(popup);
|
||||||
}
|
}
|
||||||
return null;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? _cannotShowReason(SCEntryPopupItem popup) {
|
static String? _cannotShowReason(SCEntryPopupItem popup) {
|
||||||
@ -447,7 +458,9 @@ class SCEntryPopupCoordinator {
|
|||||||
final room = await SCChatRoomRepository().specific(roomId);
|
final room = await SCChatRoomRepository().specific(roomId);
|
||||||
resolvedPreviewData = RoomEntryPreviewData.fromMyRoom(room);
|
resolvedPreviewData = RoomEntryPreviewData.fromMyRoom(room);
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
_log('open room preview failed roomId=$roomId error=$error');
|
||||||
|
}
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -521,7 +534,11 @@ class SCEntryPopupCoordinator {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _log(String message) {}
|
static void _log(String message) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('[EntryPopup] $message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String _clip(String value, {int maxLength = 180}) {
|
static String _clip(String value, {int maxLength = 180}) {
|
||||||
if (value.length <= maxLength) {
|
if (value.length <= maxLength) {
|
||||||
|
|||||||
@ -156,36 +156,59 @@ class FirstRechargeDialogData {
|
|||||||
SCFirstRechargeRewardHomeRes home,
|
SCFirstRechargeRewardHomeRes home,
|
||||||
) {
|
) {
|
||||||
final levels = home.availableLevelConfigs.take(2).toList();
|
final levels = home.availableLevelConfigs.take(2).toList();
|
||||||
final packages =
|
final packages = <FirstRechargePackage>[];
|
||||||
levels.map((level) {
|
var selectedPackageIndex = 0;
|
||||||
return FirstRechargePackage(
|
for (final level in levels) {
|
||||||
priceText: _formatPriceText(level),
|
final rewards =
|
||||||
discountText: level.discountText,
|
level.rewardItems.where(_isDisplayableRewardItem).map((item) {
|
||||||
totalValueText: level.totalValueText,
|
return FirstRechargeReward(
|
||||||
rechargeAmountCents: level.rechargeAmountCents,
|
title: _rewardTitle(item),
|
||||||
productPackage: level.productPackage,
|
style:
|
||||||
rewards:
|
item.isCoinReward
|
||||||
level.rewardItems.map((item) {
|
? FirstRechargeRewardStyle.coin
|
||||||
return FirstRechargeReward(
|
: FirstRechargeRewardStyle.prop,
|
||||||
title: item.displayTitle,
|
badgeText: item.displayBadgeText,
|
||||||
style:
|
cover: item.cover,
|
||||||
item.isCoinReward
|
);
|
||||||
? FirstRechargeRewardStyle.coin
|
}).toList();
|
||||||
: FirstRechargeRewardStyle.prop,
|
if (rewards.isEmpty) {
|
||||||
badgeText: item.displayBadgeText,
|
continue;
|
||||||
cover: item.cover,
|
}
|
||||||
);
|
if (level.current) {
|
||||||
}).toList(),
|
selectedPackageIndex = packages.length;
|
||||||
);
|
}
|
||||||
}).toList();
|
packages.add(
|
||||||
|
FirstRechargePackage(
|
||||||
|
priceText: _formatPriceText(level),
|
||||||
|
discountText: level.discountText,
|
||||||
|
totalValueText: level.totalValueText,
|
||||||
|
rechargeAmountCents: level.rechargeAmountCents,
|
||||||
|
productPackage: level.productPackage,
|
||||||
|
rewards: rewards,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final currentIndex = levels.indexWhere((level) => level.current);
|
|
||||||
return FirstRechargeDialogData(
|
return FirstRechargeDialogData(
|
||||||
packages: packages,
|
packages: packages,
|
||||||
initialPackageIndex: currentIndex >= 0 ? currentIndex : 0,
|
initialPackageIndex: selectedPackageIndex,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool _isDisplayableRewardItem(SCFirstRechargeRewardItem item) {
|
||||||
|
if (item.isCoinReward) {
|
||||||
|
return item.quantity > 0 || item.displayTitle.trim().isNotEmpty;
|
||||||
|
}
|
||||||
|
return item.name.trim().isNotEmpty || item.cover.trim().isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _rewardTitle(SCFirstRechargeRewardItem item) {
|
||||||
|
if (item.isCoinReward) {
|
||||||
|
return item.displayTitle;
|
||||||
|
}
|
||||||
|
return item.name.trim();
|
||||||
|
}
|
||||||
|
|
||||||
static String _formatPriceText(SCFirstRechargeLevelConfig level) {
|
static String _formatPriceText(SCFirstRechargeLevelConfig level) {
|
||||||
final amount = level.rechargeAmount.trim();
|
final amount = level.rechargeAmount.trim();
|
||||||
if (amount.isNotEmpty) {
|
if (amount.isNotEmpty) {
|
||||||
@ -476,14 +499,9 @@ class _FirstRechargeDialogState extends State<FirstRechargeDialog> {
|
|||||||
Offset(227, 316),
|
Offset(227, 316),
|
||||||
];
|
];
|
||||||
|
|
||||||
return List<Widget>.generate(positions.length, (index) {
|
final visibleRewards = rewards.take(positions.length).toList();
|
||||||
final reward =
|
return List<Widget>.generate(visibleRewards.length, (index) {
|
||||||
index < rewards.length
|
final reward = visibleRewards[index];
|
||||||
? rewards[index]
|
|
||||||
: const FirstRechargeReward(
|
|
||||||
title: '',
|
|
||||||
style: FirstRechargeRewardStyle.prop,
|
|
||||||
);
|
|
||||||
final position = positions[index];
|
final position = positions[index];
|
||||||
return Positioned(
|
return Positioned(
|
||||||
left: position.dx * scale,
|
left: position.dx * scale,
|
||||||
|
|||||||
@ -0,0 +1,213 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_broadcast_assets.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
|
||||||
|
|
||||||
|
class FloatingCpRelationScreenWidget extends StatefulWidget {
|
||||||
|
const FloatingCpRelationScreenWidget({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
required this.onAnimationCompleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const double bannerWidth = 350;
|
||||||
|
static const double bannerHeight = 84;
|
||||||
|
|
||||||
|
final SCFloatingMessage message;
|
||||||
|
final VoidCallback onAnimationCompleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FloatingCpRelationScreenWidget> createState() =>
|
||||||
|
_FloatingCpRelationScreenWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FloatingCpRelationScreenWidgetState
|
||||||
|
extends State<FloatingCpRelationScreenWidget>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
late final Animation<Offset> _offsetAnimation;
|
||||||
|
bool _completed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final seconds = widget.message.durationSeconds?.toInt() ?? 5;
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: Duration(seconds: seconds <= 0 ? 5 : seconds),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_offsetAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(1, 0),
|
||||||
|
end: const Offset(-1, 0),
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||||
|
_controller.addStatusListener((status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
_complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _complete() {
|
||||||
|
if (_completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_completed = true;
|
||||||
|
widget.onAnimationCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SlideTransition(
|
||||||
|
position: _offsetAnimation,
|
||||||
|
child: SizedBox(
|
||||||
|
width: FloatingCpRelationScreenWidget.bannerWidth.w,
|
||||||
|
height: FloatingCpRelationScreenWidget.bannerHeight.w,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: _buildPagBackground()),
|
||||||
|
Positioned.fill(child: _buildText()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPagBackground() {
|
||||||
|
final asset =
|
||||||
|
(widget.message.giftUrl?.trim().isNotEmpty ?? false)
|
||||||
|
? widget.message.giftUrl!.trim()
|
||||||
|
: scCpRelationBroadcastAsset(widget.message.giftTab);
|
||||||
|
return RoomRocketPagPreview(
|
||||||
|
resource: asset,
|
||||||
|
width: FloatingCpRelationScreenWidget.bannerWidth.w,
|
||||||
|
height: FloatingCpRelationScreenWidget.bannerHeight.w,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
defaultBuilder: (_) => _buildFallbackBackground(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFallbackBackground() {
|
||||||
|
final colors = _relationColors(widget.message.giftTab);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(42.w),
|
||||||
|
gradient: LinearGradient(colors: colors),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildText() {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(start: 82.w, end: 54.w),
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: ClipRect(
|
||||||
|
child: Text(
|
||||||
|
_displayText(),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: _textFontSize().sp,
|
||||||
|
height: 1.18,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
color: const Color(0xFF5B0F59).withValues(alpha: 0.86),
|
||||||
|
blurRadius: 4.w,
|
||||||
|
offset: Offset(0, 1.w),
|
||||||
|
),
|
||||||
|
Shadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.34),
|
||||||
|
blurRadius: 8.w,
|
||||||
|
offset: Offset(0, 1.5.w),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _displayText() {
|
||||||
|
if (widget.message.giftId == "RELATION_JOINED_ROOM") {
|
||||||
|
return scBuildCpRelationEntryBroadcastText(
|
||||||
|
ownerName: _shortName(widget.message.toUserName),
|
||||||
|
joinedName: _shortName(widget.message.userName),
|
||||||
|
relationType: widget.message.giftTab,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final explicitText = widget.message.displayText?.trim() ?? "";
|
||||||
|
if (explicitText.isNotEmpty) {
|
||||||
|
return _stripSystemNotificationPrefix(explicitText);
|
||||||
|
}
|
||||||
|
return _stripSystemNotificationPrefix(
|
||||||
|
scBuildCpRelationEstablishedNotice(
|
||||||
|
userName1: _shortName(widget.message.userName),
|
||||||
|
userName2: _shortName(widget.message.toUserName),
|
||||||
|
relationType: widget.message.giftTab,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _shortName(String? value) {
|
||||||
|
final text = value?.trim() ?? "";
|
||||||
|
if (text.length <= 14) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return "${text.substring(0, 13)}...";
|
||||||
|
}
|
||||||
|
|
||||||
|
double _textFontSize() {
|
||||||
|
final length = _displayText().characters.length;
|
||||||
|
if (length > 58) {
|
||||||
|
return 9.5;
|
||||||
|
}
|
||||||
|
if (length > 42) {
|
||||||
|
return 10.5;
|
||||||
|
}
|
||||||
|
return 11.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _stripSystemNotificationPrefix(String text) {
|
||||||
|
return text
|
||||||
|
.replaceFirst(
|
||||||
|
RegExp(
|
||||||
|
r'^\s*[\[\【]\s*sys(?:tem|term)\s+notification\s*[\]\】]\s*',
|
||||||
|
caseSensitive: false,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Color> _relationColors(String? relationType) {
|
||||||
|
switch (scNormalizeCpRelationType(relationType)) {
|
||||||
|
case "BROTHER":
|
||||||
|
return const [Color(0xFF0B5DA8), Color(0xFF63B8FF)];
|
||||||
|
case "SISTERS":
|
||||||
|
return const [Color(0xFF8E2BA8), Color(0xFFFF8BF3)];
|
||||||
|
case "CP":
|
||||||
|
default:
|
||||||
|
return const [Color(0xFFFF59B7), Color(0xFFFF9BD8)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,158 +1,90 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
import 'package:flutter_svga/flutter_svga.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
|
||||||
import 'package:yumi/main.dart';
|
import 'package:yumi/main.dart';
|
||||||
import 'package:marquee/marquee.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
|
|
||||||
import 'package:yumi/app_localizations.dart';
|
|
||||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/room_game_bottom_sheet.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
|
|
||||||
class FloatingGameScreenWidget extends StatefulWidget {
|
class FloatingGameScreenWidget extends StatefulWidget {
|
||||||
final SCFloatingMessage message;
|
static const String backgroundSvgaAssetPath =
|
||||||
final VoidCallback onAnimationCompleted; // 动画完成回调
|
"sc_images/room/anim/game_win.svga";
|
||||||
|
|
||||||
const FloatingGameScreenWidget({
|
const FloatingGameScreenWidget({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.onAnimationCompleted,
|
required this.onAnimationCompleted,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
|
final SCFloatingMessage message;
|
||||||
|
final VoidCallback onAnimationCompleted;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FloatingGameScreenWidgetState createState() =>
|
State<FloatingGameScreenWidget> createState() =>
|
||||||
_FloatingGameScreenWidgetState();
|
_FloatingGameScreenWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
|
class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
Debouncer debouncer = Debouncer();
|
static const String _avatarDynamicKey = "avatar";
|
||||||
late AnimationController _controller;
|
static const String _gameIconDynamicKey = "gameicon";
|
||||||
late Animation<Offset> _firstAnimation; // 第一段:从右到中
|
static const String _textLine1DynamicKey = "text1";
|
||||||
late Animation<Offset> _secondAnimation; // 第二段:从中到左
|
static const String _textLine2DynamicKey = "text2";
|
||||||
late AnimationController _swipeController; // 新增:滑动动画控制器
|
|
||||||
late Animation<Offset> _swipeAnimation; // 新增:滑动动画
|
|
||||||
|
|
||||||
bool _firstPhaseCompleted = false;
|
final Debouncer _debouncer = Debouncer();
|
||||||
bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画
|
late final AnimationController _controller;
|
||||||
|
late final Animation<Offset> _offsetAnimation;
|
||||||
|
late final AnimationController _swipeController;
|
||||||
|
late final Animation<Offset> _swipeAnimation;
|
||||||
|
|
||||||
///游戏赢的金币按数量分等级,不同等级特效背景也不一样
|
bool _isSwipeAnimating = false;
|
||||||
int level = 1;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if ((widget.message.coins ?? 0) > 14999 &&
|
|
||||||
(widget.message.coins ?? 0) < 50000) {
|
|
||||||
level = 1;
|
|
||||||
} else if ((widget.message.coins ?? 0) > 49999 &&
|
|
||||||
(widget.message.coins ?? 0) < 100000) {
|
|
||||||
level = 2;
|
|
||||||
} else if ((widget.message.coins ?? 0) > 99999 &&
|
|
||||||
(widget.message.coins ?? 0) < 300000) {
|
|
||||||
level = 3;
|
|
||||||
} else if ((widget.message.coins ?? 0) > 299999 &&
|
|
||||||
(widget.message.coins ?? 0) < 500000) {
|
|
||||||
level = 4;
|
|
||||||
} else if ((widget.message.coins ?? 0) > 499999) {
|
|
||||||
level = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主动画控制器
|
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
duration: const Duration(milliseconds: 1500), // 每段动画1.5秒
|
duration: const Duration(seconds: 5),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 滑动动画控制器
|
|
||||||
_swipeController = AnimationController(
|
_swipeController = AnimationController(
|
||||||
duration: const Duration(milliseconds: 550), // 滑动动画500ms
|
duration: const Duration(milliseconds: 550),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
_offsetAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(1.0, 0.0),
|
||||||
|
end: const Offset(-1.0, 0.0),
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||||
|
_swipeAnimation = Tween<Offset>(
|
||||||
|
begin: Offset.zero,
|
||||||
|
end: const Offset(-1.5, 0.0),
|
||||||
|
).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn));
|
||||||
|
|
||||||
// 监听滑动动画完成
|
_controller.addStatusListener((status) {
|
||||||
|
if (status == AnimationStatus.completed && !_isSwipeAnimating) {
|
||||||
|
widget.onAnimationCompleted();
|
||||||
|
}
|
||||||
|
});
|
||||||
_swipeController.addStatusListener((status) {
|
_swipeController.addStatusListener((status) {
|
||||||
if (status == AnimationStatus.completed) {
|
if (status == AnimationStatus.completed) {
|
||||||
widget.onAnimationCompleted();
|
widget.onAnimationCompleted();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 第一段动画:从右侧飘到中间
|
|
||||||
_firstAnimation = Tween<Offset>(
|
|
||||||
begin: const Offset(1.5, 0.0), // 从屏幕右侧外开始
|
|
||||||
end: Offset.zero, // 到屏幕中心
|
|
||||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
|
||||||
|
|
||||||
// 第二段动画:从中间飘到左侧
|
|
||||||
_secondAnimation = Tween<Offset>(
|
|
||||||
begin: Offset.zero, // 从屏幕中心开始
|
|
||||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
|
||||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
|
||||||
|
|
||||||
// 滑动动画:从当前位置滑动到左侧屏幕外
|
|
||||||
_swipeAnimation = Tween<Offset>(
|
|
||||||
begin: Offset.zero, // 从当前位置开始
|
|
||||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: _swipeController,
|
|
||||||
curve: Curves.easeIn,
|
|
||||||
));
|
|
||||||
|
|
||||||
// 延迟启动动画
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_startAnimationSequence();
|
_controller.forward();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startAnimationSequence() async {
|
|
||||||
// 第一阶段:从右到中
|
|
||||||
_controller.reset();
|
|
||||||
await _controller.forward();
|
|
||||||
|
|
||||||
// 停顿2秒(你代码中是3秒,这里保持原样)
|
|
||||||
await Future.delayed(const Duration(seconds: 3));
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
// 第二阶段:从中到左
|
|
||||||
setState(() {
|
|
||||||
_firstPhaseCompleted = true;
|
|
||||||
});
|
|
||||||
_controller.reset();
|
|
||||||
await _controller.forward();
|
|
||||||
|
|
||||||
if (mounted && !_isSwipeAnimating) {
|
|
||||||
widget.onAnimationCompleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理向左滑动
|
|
||||||
void _handleSwipeLeft() {
|
|
||||||
if (_isSwipeAnimating) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isSwipeAnimating = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 停止主动画
|
|
||||||
_controller.stop();
|
|
||||||
|
|
||||||
// 启动滑动动画
|
|
||||||
_swipeController.reset();
|
|
||||||
_swipeController.forward();
|
|
||||||
}
|
|
||||||
|
|
||||||
String _coinsVaFormat(num coins) {
|
|
||||||
int value = coins.toInt();
|
|
||||||
if (value > 99999) {
|
|
||||||
return "${(value / 1000).toStringAsFixed(0)}k";
|
|
||||||
}
|
|
||||||
return "$value";
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
@ -162,251 +94,391 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _buildGameAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildGameAnimation() {
|
|
||||||
// 如果正在执行滑动动画,使用滑动动画
|
|
||||||
if (_isSwipeAnimating) {
|
|
||||||
return SlideTransition(
|
|
||||||
position: _swipeAnimation,
|
|
||||||
child: _buildContent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则使用原来的动画
|
|
||||||
return SlideTransition(
|
|
||||||
position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation,
|
|
||||||
child: _buildContent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildContent() {
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: _handleTap,
|
||||||
debouncer.debounce(
|
onHorizontalDragEnd: _handleHorizontalDragEnd,
|
||||||
duration: Duration(milliseconds: 350),
|
child: SlideTransition(
|
||||||
onDebounce: () {
|
position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation,
|
||||||
if (widget.message.roomId != null &&
|
child: SizedBox(
|
||||||
widget.message.roomId!.isNotEmpty) {
|
width: 350.w,
|
||||||
SCRoomUtils.goRoom(
|
height: 128.w,
|
||||||
widget.message.roomId!,
|
child: SCSvgaAssetWidget(
|
||||||
navigatorKey.currentState!.context,
|
assetPath: FloatingGameScreenWidget.backgroundSvgaAssetPath,
|
||||||
fromFloting: true,
|
width: 350.w,
|
||||||
);
|
height: 128.w,
|
||||||
}
|
fit: BoxFit.fill,
|
||||||
},
|
dynamicIdentity: [
|
||||||
);
|
widget.message.userAvatarUrl ?? "",
|
||||||
},
|
widget.message.userName ?? "",
|
||||||
onHorizontalDragEnd: (details) {
|
widget.message.userId ?? "",
|
||||||
// 获取当前的文本方向
|
widget.message.giftUrl ?? "",
|
||||||
final textDirection = Directionality.of(context);
|
widget.message.gameId ?? "",
|
||||||
|
widget.message.displayText ?? "",
|
||||||
// 定义一个根据方向转换速度符号的辅助函数
|
widget.message.coins ?? 0,
|
||||||
double effectiveVelocity(double velocity) {
|
widget.message.multiple ?? 0,
|
||||||
// 在RTL模式下,反转速度的正负号
|
].join("|"),
|
||||||
return textDirection == TextDirection.rtl
|
movieConfigurer: _configureGameWinSvga,
|
||||||
? -velocity
|
fallback: _buildFallbackBanner(),
|
||||||
: velocity;
|
|
||||||
}
|
|
||||||
|
|
||||||
double velocity = effectiveVelocity(details.primaryVelocity ?? 0);
|
|
||||||
if (velocity < 0) {
|
|
||||||
///向左滑动,把当前视图向左平移出去
|
|
||||||
_handleSwipeLeft();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: Directionality(
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
height: 120.w,
|
|
||||||
width: 325.w,
|
|
||||||
margin: EdgeInsets.only(top: 10.w),
|
|
||||||
child: Stack(
|
|
||||||
alignment:
|
|
||||||
level == 5
|
|
||||||
? AlignmentDirectional.topCenter
|
|
||||||
: AlignmentDirectional.center,
|
|
||||||
children: [
|
|
||||||
if (widget.message.userAvatarUrl?.isNotEmpty ?? false)
|
|
||||||
_buildHead(),
|
|
||||||
Image.asset("sc_images/index/sc_icon_gamebroad_lv${level}_bg.webp"),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min, // 宽度由内容决定
|
|
||||||
children: [
|
|
||||||
SizedBox(width: level < 2 ? 82.w : 95.w),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height:
|
|
||||||
level < 2
|
|
||||||
? 22.w
|
|
||||||
: (level == 5 ? 31.w : 25.w),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: 55.w,
|
|
||||||
maxHeight: 18.w,
|
|
||||||
),
|
|
||||||
child:
|
|
||||||
(widget.message.userName?.length ??
|
|
||||||
0) >
|
|
||||||
8
|
|
||||||
? Marquee(
|
|
||||||
text:
|
|
||||||
widget.message.userName ??
|
|
||||||
"",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12.sp,
|
|
||||||
color: Colors.orange,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
decoration:
|
|
||||||
TextDecoration.none,
|
|
||||||
),
|
|
||||||
scrollAxis: Axis.horizontal,
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
blankSpace: 20.0,
|
|
||||||
velocity: 40.0,
|
|
||||||
pauseAfterRound: Duration(
|
|
||||||
seconds: 1,
|
|
||||||
),
|
|
||||||
accelerationDuration: Duration(
|
|
||||||
seconds: 1,
|
|
||||||
),
|
|
||||||
accelerationCurve:
|
|
||||||
Curves.easeOut,
|
|
||||||
decelerationDuration: Duration(
|
|
||||||
milliseconds: 500,
|
|
||||||
),
|
|
||||||
decelerationCurve:
|
|
||||||
Curves.easeOut,
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
widget.message.userName ?? "",
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12.sp,
|
|
||||||
color: Colors.orange,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
decoration:
|
|
||||||
TextDecoration.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
text(
|
|
||||||
"ID:${widget.message.userId} ",
|
|
||||||
textColor: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12.sp,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0,
|
|
||||||
level < 2 ? -5 : (level == 5 ? -5 : 0),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
text(
|
|
||||||
SCAppLocalizations.of(context)!.get,
|
|
||||||
textColor: Colors.orange,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontStyle: FontStyle.italic
|
|
||||||
),
|
|
||||||
buildNumForGame(
|
|
||||||
_coinsVaFormat(
|
|
||||||
widget.message.coins ?? 0,
|
|
||||||
),
|
|
||||||
size:
|
|
||||||
(widget.message.coins ?? 0) > 99999
|
|
||||||
? 26.w
|
|
||||||
: 20.w,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(top: level < 2 ? 10.w : 21.w),
|
|
||||||
child: netImage(
|
|
||||||
url: widget.message.giftUrl ?? "",
|
|
||||||
width: 28.w,
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(3.0)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: level < 2 ? 63.w : 65.w),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHead() {
|
void _handleTap() {
|
||||||
if (level == 1) {
|
_debouncer.debounce(
|
||||||
return PositionedDirectional(
|
duration: const Duration(milliseconds: 350),
|
||||||
start: 43.w,
|
onDebounce: () {
|
||||||
child: Container(
|
final currentContext = navigatorKey.currentState?.context;
|
||||||
margin: EdgeInsets.only(top: 12.w),
|
if (currentContext == null || !currentContext.mounted) {
|
||||||
child: head(url: widget.message.userAvatarUrl ?? "", width: 40.w),
|
return;
|
||||||
),
|
}
|
||||||
);
|
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||||
} else if (level == 2) {
|
currentContext,
|
||||||
return PositionedDirectional(
|
listen: false,
|
||||||
start: 10.w,
|
);
|
||||||
child: Container(
|
final currentRoomId =
|
||||||
margin: EdgeInsets.only(top: 25.w),
|
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||||
child: head(url: widget.message.userAvatarUrl ?? "", width: 70.w),
|
final messageRoomId = (widget.message.roomId ?? "").trim();
|
||||||
),
|
final isSameVisibleRoom =
|
||||||
);
|
rtcProvider.shouldShowRoomVisualEffects &&
|
||||||
} else if (level == 3) {
|
rtcProvider.isVoiceRoomRouteVisible &&
|
||||||
return PositionedDirectional(
|
currentRoomId.isNotEmpty &&
|
||||||
start: 10.w,
|
(messageRoomId.isEmpty || currentRoomId == messageRoomId);
|
||||||
child: Container(
|
|
||||||
margin: EdgeInsets.only(top: 25.w),
|
if (isSameVisibleRoom) {
|
||||||
child: head(url: widget.message.userAvatarUrl ?? "", width: 70.w),
|
showRoomGameListSheet(
|
||||||
),
|
currentContext,
|
||||||
);
|
initialGameId: _resolvedGameId(),
|
||||||
} else if (level == 4) {
|
);
|
||||||
return PositionedDirectional(
|
return;
|
||||||
start: 10.w,
|
}
|
||||||
child: Container(
|
if (messageRoomId.isNotEmpty) {
|
||||||
margin: EdgeInsets.only(top: 25.w),
|
SCRoomUtils.goRoom(messageRoomId, currentContext, fromFloting: true);
|
||||||
child: head(url: widget.message.userAvatarUrl ?? "", width: 70.w),
|
}
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
} else if (level == 5) {
|
}
|
||||||
return PositionedDirectional(
|
|
||||||
start: 22.w,
|
void _handleHorizontalDragEnd(DragEndDetails details) {
|
||||||
top: 20.w,
|
final textDirection = Directionality.of(context);
|
||||||
child: Container(
|
final velocity =
|
||||||
child: head(url: widget.message.userAvatarUrl ?? "", width: 48.w),
|
textDirection == TextDirection.rtl
|
||||||
),
|
? -(details.primaryVelocity ?? 0)
|
||||||
);
|
: (details.primaryVelocity ?? 0);
|
||||||
|
if (velocity < 0) {
|
||||||
|
_handleSwipeLeft();
|
||||||
}
|
}
|
||||||
return Container();
|
}
|
||||||
|
|
||||||
|
void _handleSwipeLeft() {
|
||||||
|
if (_isSwipeAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isSwipeAnimating = true;
|
||||||
|
});
|
||||||
|
_controller.stop();
|
||||||
|
_swipeController.reset();
|
||||||
|
_swipeController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _configureGameWinSvga(MovieEntity movieEntity) async {
|
||||||
|
final avatarUrl = (widget.message.userAvatarUrl ?? "").trim();
|
||||||
|
if (avatarUrl.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final avatarImage = await _loadUiImage(
|
||||||
|
avatarUrl,
|
||||||
|
logicalWidth: 96,
|
||||||
|
logicalHeight: 96,
|
||||||
|
);
|
||||||
|
if (avatarImage != null) {
|
||||||
|
movieEntity.dynamicItem.setImage(avatarImage, _avatarDynamicKey);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final gameIconUrl = (widget.message.giftUrl ?? "").trim();
|
||||||
|
if (gameIconUrl.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final gameIcon = await _loadUiImage(
|
||||||
|
gameIconUrl,
|
||||||
|
logicalWidth: 96,
|
||||||
|
logicalHeight: 96,
|
||||||
|
);
|
||||||
|
if (gameIcon != null) {
|
||||||
|
movieEntity.dynamicItem.setImage(gameIcon, _gameIconDynamicKey);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final lines = _resolveTextLines();
|
||||||
|
try {
|
||||||
|
movieEntity.dynamicItem.setImage(
|
||||||
|
await _buildTextLineImage(lines.$1, highlighted: false),
|
||||||
|
_textLine1DynamicKey,
|
||||||
|
);
|
||||||
|
movieEntity.dynamicItem.setImage(
|
||||||
|
await _buildTextLineImage(lines.$2, highlighted: true),
|
||||||
|
_textLine2DynamicKey,
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, String) _resolveTextLines() {
|
||||||
|
final rawText = (widget.message.displayText ?? "").trim();
|
||||||
|
if (rawText.isNotEmpty) {
|
||||||
|
final normalized = rawText.replaceAll(RegExp(r"\s+"), " ");
|
||||||
|
final winIndex = normalized.indexOf(" win ");
|
||||||
|
if (winIndex > 0 && winIndex < normalized.length - 5) {
|
||||||
|
return (
|
||||||
|
normalized.substring(0, winIndex).trim(),
|
||||||
|
normalized.substring(winIndex + 1).trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _splitText(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
final userName = _firstNonBlank([
|
||||||
|
widget.message.userName,
|
||||||
|
widget.message.userId,
|
||||||
|
]);
|
||||||
|
final gameName = _firstNonBlank([widget.message.toUserName, "game"]);
|
||||||
|
final multiple = _formatMultiple(widget.message.multiple);
|
||||||
|
final coins = _formatCoins(widget.message.coins);
|
||||||
|
final line1 =
|
||||||
|
userName.isEmpty
|
||||||
|
? "play $gameName get x $multiple"
|
||||||
|
: "$userName play $gameName get x $multiple";
|
||||||
|
return (line1, "win $coins coins");
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, String) _splitText(String text) {
|
||||||
|
const maxLineLength = 32;
|
||||||
|
if (text.length <= maxLineLength) {
|
||||||
|
return (text, "");
|
||||||
|
}
|
||||||
|
final midpoint = (text.length / 2).round();
|
||||||
|
var splitIndex = text.lastIndexOf(" ", midpoint);
|
||||||
|
if (splitIndex < 10) {
|
||||||
|
splitIndex = text.indexOf(" ", midpoint);
|
||||||
|
}
|
||||||
|
if (splitIndex < 10 || splitIndex >= text.length - 1) {
|
||||||
|
splitIndex = midpoint;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
text.substring(0, splitIndex).trim(),
|
||||||
|
text.substring(splitIndex).trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.Image> _buildTextLineImage(
|
||||||
|
String value, {
|
||||||
|
required bool highlighted,
|
||||||
|
}) async {
|
||||||
|
const double logicalWidth = 318;
|
||||||
|
const double logicalHeight = 24;
|
||||||
|
const double pixelRatio = 3;
|
||||||
|
|
||||||
|
final recorder = ui.PictureRecorder();
|
||||||
|
final canvas = Canvas(recorder);
|
||||||
|
canvas.scale(pixelRatio, pixelRatio);
|
||||||
|
canvas.clipRect(const Rect.fromLTWH(0, 0, logicalWidth, logicalHeight));
|
||||||
|
|
||||||
|
final painter = TextPainter(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
maxLines: 1,
|
||||||
|
ellipsis: "...",
|
||||||
|
text: TextSpan(
|
||||||
|
text: value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: highlighted ? 18 : 16,
|
||||||
|
height: 1,
|
||||||
|
color:
|
||||||
|
highlighted ? const Color(0xFFFFE95A) : const Color(0xFFFFFFFF),
|
||||||
|
fontWeight: highlighted ? FontWeight.w900 : FontWeight.w800,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)..layout(maxWidth: logicalWidth - 8);
|
||||||
|
|
||||||
|
painter.paint(canvas, Offset(4, (logicalHeight - painter.height) / 2));
|
||||||
|
final picture = recorder.endRecording();
|
||||||
|
return picture.toImage(
|
||||||
|
(logicalWidth * pixelRatio).round(),
|
||||||
|
(logicalHeight * pixelRatio).round(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.Image?> _loadUiImage(
|
||||||
|
String resource, {
|
||||||
|
double? logicalWidth,
|
||||||
|
double? logicalHeight,
|
||||||
|
}) async {
|
||||||
|
final target = resource.trim();
|
||||||
|
if (target.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final provider = buildCachedImageProvider(
|
||||||
|
target,
|
||||||
|
logicalWidth: logicalWidth,
|
||||||
|
logicalHeight: logicalHeight,
|
||||||
|
);
|
||||||
|
final stream = provider.resolve(ImageConfiguration.empty);
|
||||||
|
final completer = Completer<ui.Image?>();
|
||||||
|
late final ImageStreamListener listener;
|
||||||
|
listener = ImageStreamListener(
|
||||||
|
(imageInfo, synchronousCall) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(imageInfo.image);
|
||||||
|
}
|
||||||
|
stream.removeListener(listener);
|
||||||
|
},
|
||||||
|
onError: (Object error, StackTrace? stackTrace) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(null);
|
||||||
|
}
|
||||||
|
stream.removeListener(listener);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
stream.addListener(listener);
|
||||||
|
return completer.future.timeout(
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
onTimeout: () {
|
||||||
|
stream.removeListener(listener);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFallbackBanner() {
|
||||||
|
return Container(
|
||||||
|
width: 350.w,
|
||||||
|
height: 128.w,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 26.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xDD291436),
|
||||||
|
borderRadius: BorderRadius.circular(8.w),
|
||||||
|
border: Border.all(color: const Color(0xFFFFD35A), width: 1.w),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildFallbackImage(
|
||||||
|
widget.message.userAvatarUrl,
|
||||||
|
size: 46.w,
|
||||||
|
circular: true,
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_resolveTextLines().$1,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13.sp,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 3.w),
|
||||||
|
Text(
|
||||||
|
_resolveTextLines().$2,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: const Color(0xFFFFE95A),
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
_buildFallbackImage(widget.message.giftUrl, size: 40.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFallbackImage(
|
||||||
|
String? resource, {
|
||||||
|
required double size,
|
||||||
|
bool circular = false,
|
||||||
|
}) {
|
||||||
|
final target = (resource ?? "").trim();
|
||||||
|
final fallbackAsset =
|
||||||
|
circular
|
||||||
|
? "sc_images/general/sc_icon_avar_defalt.png"
|
||||||
|
: "sc_images/room/sc_icon_botton_game.png";
|
||||||
|
final child =
|
||||||
|
target.isEmpty
|
||||||
|
? Image.asset(
|
||||||
|
fallbackAsset,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: Image(
|
||||||
|
image: buildCachedImageProvider(
|
||||||
|
target,
|
||||||
|
logicalWidth: size,
|
||||||
|
logicalHeight: size,
|
||||||
|
),
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(_, __, ___) => Image.asset(
|
||||||
|
fallbackAsset,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!circular) {
|
||||||
|
return ClipRRect(borderRadius: BorderRadius.circular(6.w), child: child);
|
||||||
|
}
|
||||||
|
return ClipOval(child: child);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _resolvedGameId() {
|
||||||
|
final gameId = (widget.message.gameId ?? "").trim();
|
||||||
|
return gameId.isEmpty ? null : gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatCoins(num? value) {
|
||||||
|
final coins = value ?? 0;
|
||||||
|
if (coins >= 100000) {
|
||||||
|
return "${(coins / 1000).toStringAsFixed(0)}k";
|
||||||
|
}
|
||||||
|
if (coins == coins.roundToDouble()) {
|
||||||
|
return coins.toInt().toString();
|
||||||
|
}
|
||||||
|
return coins.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatMultiple(num? value) {
|
||||||
|
final multiple = value ?? 0;
|
||||||
|
if (multiple == multiple.roundToDouble()) {
|
||||||
|
return multiple.toInt().toString();
|
||||||
|
}
|
||||||
|
return multiple.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _firstNonBlank(Iterable<String?> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.trim() ?? "";
|
||||||
|
if (text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -368,6 +368,14 @@ class _RocketRuleHtmlText extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String debugRoomRocketRuleHtmlPlainText(String html) {
|
||||||
|
final spans = _RocketRuleHtmlParser(
|
||||||
|
baseStyle: const TextStyle(),
|
||||||
|
scale: 1,
|
||||||
|
).parse(html);
|
||||||
|
return TextSpan(children: spans).toPlainText();
|
||||||
|
}
|
||||||
|
|
||||||
class _RocketRuleHtmlParser {
|
class _RocketRuleHtmlParser {
|
||||||
_RocketRuleHtmlParser({required this.baseStyle, required this.scale});
|
_RocketRuleHtmlParser({required this.baseStyle, required this.scale});
|
||||||
|
|
||||||
@ -446,9 +454,12 @@ class _RocketRuleHtmlParser {
|
|||||||
case 'li':
|
case 'li':
|
||||||
_appendBlockBreak();
|
_appendBlockBreak();
|
||||||
final list = _listStack.isEmpty ? null : _listStack.last;
|
final list = _listStack.isEmpty ? null : _listStack.last;
|
||||||
final prefix =
|
if (list == null) {
|
||||||
list == null || !list.ordered ? '- ' : '${list.next()}. ';
|
_appendRaw('- ', _currentStyle);
|
||||||
_appendRaw(prefix, _currentStyle);
|
} else {
|
||||||
|
final prefix = list.ordered ? '${list.next()}. ' : '- ';
|
||||||
|
list.beginItem(prefix);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'p':
|
case 'p':
|
||||||
case 'div':
|
case 'div':
|
||||||
@ -477,7 +488,10 @@ class _RocketRuleHtmlParser {
|
|||||||
_appendBlockBreak();
|
_appendBlockBreak();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_isBlockTag(tagName) || tagName == 'li') {
|
if (tagName == 'li') {
|
||||||
|
_endCurrentListItem();
|
||||||
|
_appendBlockBreak();
|
||||||
|
} else if (_isBlockTag(tagName)) {
|
||||||
_appendBlockBreak();
|
_appendBlockBreak();
|
||||||
}
|
}
|
||||||
for (var index = _styleStack.length - 1; index >= 0; index -= 1) {
|
for (var index = _styleStack.length - 1; index >= 0; index -= 1) {
|
||||||
@ -588,7 +602,18 @@ class _RocketRuleHtmlParser {
|
|||||||
if (normalized.isEmpty) {
|
if (normalized.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_appendRaw(normalized, _currentStyle);
|
if (normalized.trim().isEmpty) {
|
||||||
|
if (_segments.isEmpty ||
|
||||||
|
_segments.last.text.endsWith('\n') ||
|
||||||
|
_pendingListFrame != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_appendRaw(' ', _currentStyle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final style = _currentStyle;
|
||||||
|
final textAfterMarker = _flushPendingListMarker(normalized, style);
|
||||||
|
_appendRaw(textAfterMarker, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _appendRaw(String text, TextStyle style) {
|
void _appendRaw(String text, TextStyle style) {
|
||||||
@ -612,6 +637,9 @@ class _RocketRuleHtmlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _appendBlockBreak() {
|
void _appendBlockBreak() {
|
||||||
|
if (_pendingListFrame != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_segments.isEmpty) {
|
if (_segments.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -622,6 +650,55 @@ class _RocketRuleHtmlParser {
|
|||||||
_appendRaw('\n', _currentStyle);
|
_appendRaw('\n', _currentStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _flushPendingListMarker(String text, TextStyle style) {
|
||||||
|
final list = _pendingListFrame;
|
||||||
|
final marker = list?.pendingMarker;
|
||||||
|
if (list == null || marker == null) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
final leading = RegExp(r'^\s*').firstMatch(text)?.group(0) ?? '';
|
||||||
|
final visibleText = text.substring(leading.length);
|
||||||
|
if (visibleText.isEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
list.suppressGeneratedMarkers ??= _hasManualListMarker(visibleText);
|
||||||
|
list.pendingMarker = null;
|
||||||
|
if (list.suppressGeneratedMarkers! ||
|
||||||
|
_startsWithGeneratedMarker(visibleText, marker)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
_appendRaw(marker, style);
|
||||||
|
return visibleText;
|
||||||
|
}
|
||||||
|
|
||||||
|
_HtmlListFrame? get _pendingListFrame {
|
||||||
|
for (var index = _listStack.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (_listStack[index].pendingMarker != null) {
|
||||||
|
return _listStack[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _endCurrentListItem() {
|
||||||
|
for (var index = _listStack.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (_listStack[index].isInItem) {
|
||||||
|
_listStack[index].endItem();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasManualListMarker(String text) {
|
||||||
|
return RegExp(r'^(?:\d+|[a-zA-Z])[.)](?:\s|$)').hasMatch(text) ||
|
||||||
|
RegExp(r'^(?:[-*•‣◦])(?:\s|$)').hasMatch(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _startsWithGeneratedMarker(String text, String marker) {
|
||||||
|
return text.startsWith(marker) ||
|
||||||
|
text.startsWith(marker.trimRight()) && text.length == marker.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
void _trimOuterWhitespace() {
|
void _trimOuterWhitespace() {
|
||||||
while (_segments.isNotEmpty) {
|
while (_segments.isNotEmpty) {
|
||||||
final trimmed = _segments.first.text.replaceFirst(RegExp(r'^\s+'), '');
|
final trimmed = _segments.first.text.replaceFirst(RegExp(r'^\s+'), '');
|
||||||
@ -791,6 +868,19 @@ class _HtmlListFrame {
|
|||||||
|
|
||||||
final bool ordered;
|
final bool ordered;
|
||||||
int _index = 1;
|
int _index = 1;
|
||||||
|
String? pendingMarker;
|
||||||
|
bool? suppressGeneratedMarkers;
|
||||||
|
bool isInItem = false;
|
||||||
|
|
||||||
|
void beginItem(String marker) {
|
||||||
|
isInItem = true;
|
||||||
|
pendingMarker = marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
void endItem() {
|
||||||
|
isInItem = false;
|
||||||
|
pendingMarker = null;
|
||||||
|
}
|
||||||
|
|
||||||
int next() {
|
int next() {
|
||||||
final value = _index;
|
final value = _index;
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:pag/pag.dart';
|
import 'package:pag/pag.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||||
|
|
||||||
const Duration _pagMemoryTtl = Duration(minutes: 5);
|
const Duration _pagMemoryTtl = Duration(minutes: 5);
|
||||||
const Size _roomRocketFallbackPagRawSize = Size(750, 1624);
|
const Duration _roomRocketTopAvatarResolveGracePeriod = Duration(
|
||||||
const Rect _roomRocketTopAvatarRawRect = Rect.fromLTWH(291, 164, 168, 168);
|
milliseconds: 700,
|
||||||
|
);
|
||||||
|
const String _roomRocketTopAvatarLayerName = 'avatar_tip.png';
|
||||||
|
|
||||||
class RoomRocketPagEffectOverlay extends StatefulWidget {
|
class RoomRocketPagEffectOverlay extends StatefulWidget {
|
||||||
const RoomRocketPagEffectOverlay({
|
const RoomRocketPagEffectOverlay({
|
||||||
@ -54,7 +55,40 @@ class RoomRocketPagEffectOverlay extends StatefulWidget {
|
|||||||
class _RoomRocketPagEffectOverlayState
|
class _RoomRocketPagEffectOverlayState
|
||||||
extends State<RoomRocketPagEffectOverlay> {
|
extends State<RoomRocketPagEffectOverlay> {
|
||||||
bool _completed = false;
|
bool _completed = false;
|
||||||
Size? _pagRawSize;
|
bool _topAvatarLocked = false;
|
||||||
|
String? _lockedTopAvatarUrl;
|
||||||
|
Timer? _topAvatarLockTimer;
|
||||||
|
ValueListenable<String?>? _observedTopAvatarListenable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_observeTopAvatarListenable(widget.topAvatarUrlListenable);
|
||||||
|
_resolveTopAvatarLock(notify: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant RoomRocketPagEffectOverlay oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final listenableChanged =
|
||||||
|
oldWidget.topAvatarUrlListenable != widget.topAvatarUrlListenable;
|
||||||
|
final fallbackAvatarChanged = oldWidget.topAvatarUrl != widget.topAvatarUrl;
|
||||||
|
final resourceChanged = oldWidget.resource.trim() != widget.resource.trim();
|
||||||
|
if (listenableChanged) {
|
||||||
|
_observeTopAvatarListenable(widget.topAvatarUrlListenable);
|
||||||
|
}
|
||||||
|
if (resourceChanged || listenableChanged || fallbackAvatarChanged) {
|
||||||
|
_resetTopAvatarLock();
|
||||||
|
_resolveTopAvatarLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_topAvatarLockTimer?.cancel();
|
||||||
|
_observedTopAvatarListenable?.removeListener(_handleTopAvatarChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -67,28 +101,14 @@ class _RoomRocketPagEffectOverlayState
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
child: Stack(
|
child:
|
||||||
clipBehavior: Clip.none,
|
_topAvatarLocked
|
||||||
children: [
|
? _buildPagViewWithAvatar(
|
||||||
_buildTopAvatar(size),
|
resource: resource,
|
||||||
Positioned.fill(
|
size: size,
|
||||||
child: _RoomRocketPagView(
|
avatarUrl: _lockedTopAvatarUrl,
|
||||||
resource: resource,
|
)
|
||||||
width: size.width,
|
: const Center(child: SCRotatingDotsLoading()),
|
||||||
height: size.height,
|
|
||||||
loop: false,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
displayScale: 1,
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
memoryTtl: _pagMemoryTtl,
|
|
||||||
onReady: _updatePagRawSize,
|
|
||||||
onCompleted: _complete,
|
|
||||||
defaultBuilder:
|
|
||||||
(_) => const Center(child: SCRotatingDotsLoading()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -103,69 +123,77 @@ class _RoomRocketPagEffectOverlayState
|
|||||||
widget.onCompleted?.call();
|
widget.onCompleted?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updatePagRawSize(Size rawSize) {
|
void _observeTopAvatarListenable(ValueListenable<String?>? listenable) {
|
||||||
if (_pagRawSize == rawSize) {
|
if (_observedTopAvatarListenable == listenable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() => _pagRawSize = rawSize);
|
_observedTopAvatarListenable?.removeListener(_handleTopAvatarChanged);
|
||||||
|
_observedTopAvatarListenable = listenable;
|
||||||
|
listenable?.addListener(_handleTopAvatarChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTopAvatar(Size canvasSize) {
|
void _handleTopAvatarChanged() {
|
||||||
final listenable = widget.topAvatarUrlListenable;
|
if (_topAvatarLocked) {
|
||||||
if (listenable == null) {
|
return;
|
||||||
return _RoomRocketPagTopAvatar(
|
}
|
||||||
avatarUrl: widget.topAvatarUrl,
|
final avatarUrl = _currentTopAvatarUrl();
|
||||||
canvasSize: canvasSize,
|
if (avatarUrl != null) {
|
||||||
pagRawSize: _pagRawSize,
|
_lockTopAvatar(avatarUrl);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return ValueListenableBuilder<String?>(
|
|
||||||
valueListenable: listenable,
|
|
||||||
builder:
|
|
||||||
(context, avatarUrl, _) => _RoomRocketPagTopAvatar(
|
|
||||||
avatarUrl: _firstNonBlankPagAvatar([
|
|
||||||
avatarUrl,
|
|
||||||
widget.topAvatarUrl,
|
|
||||||
]),
|
|
||||||
canvasSize: canvasSize,
|
|
||||||
pagRawSize: _pagRawSize,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _RoomRocketPagTopAvatar extends StatelessWidget {
|
void _resolveTopAvatarLock({bool notify = true}) {
|
||||||
const _RoomRocketPagTopAvatar({
|
final avatarUrl = _currentTopAvatarUrl();
|
||||||
required this.avatarUrl,
|
if (avatarUrl != null || widget.topAvatarUrlListenable == null) {
|
||||||
required this.canvasSize,
|
_lockTopAvatar(avatarUrl, notify: notify);
|
||||||
required this.pagRawSize,
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
final String? avatarUrl;
|
|
||||||
final Size canvasSize;
|
|
||||||
final Size? pagRawSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final url = avatarUrl?.trim() ?? '';
|
|
||||||
if (url.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
}
|
||||||
final rect = _mapRoomRocketPagRawRectToCoverCanvas(
|
_topAvatarLockTimer?.cancel();
|
||||||
rawRect: _roomRocketTopAvatarRawRect,
|
_topAvatarLockTimer = Timer(_roomRocketTopAvatarResolveGracePeriod, () {
|
||||||
rawSize: pagRawSize ?? _roomRocketFallbackPagRawSize,
|
_lockTopAvatar(_currentTopAvatarUrl());
|
||||||
canvasSize: canvasSize,
|
});
|
||||||
);
|
}
|
||||||
return Positioned.fromRect(
|
|
||||||
rect: rect,
|
void _lockTopAvatar(String? avatarUrl, {bool notify = true}) {
|
||||||
child: ClipOval(
|
_topAvatarLockTimer?.cancel();
|
||||||
child: CustomCachedImage(
|
final normalized = _firstNonBlankPagAvatar([avatarUrl]);
|
||||||
imageUrl: url,
|
_topAvatarLocked = true;
|
||||||
fit: BoxFit.cover,
|
_lockedTopAvatarUrl = normalized;
|
||||||
placeholder: const SizedBox.shrink(),
|
if (notify && mounted) {
|
||||||
errorWidget: const SizedBox.shrink(),
|
setState(() {});
|
||||||
),
|
}
|
||||||
),
|
}
|
||||||
|
|
||||||
|
void _resetTopAvatarLock() {
|
||||||
|
_topAvatarLockTimer?.cancel();
|
||||||
|
_topAvatarLocked = false;
|
||||||
|
_lockedTopAvatarUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _currentTopAvatarUrl() {
|
||||||
|
return _firstNonBlankPagAvatar([
|
||||||
|
widget.topAvatarUrlListenable?.value,
|
||||||
|
widget.topAvatarUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPagViewWithAvatar({
|
||||||
|
required String resource,
|
||||||
|
required Size size,
|
||||||
|
required String? avatarUrl,
|
||||||
|
}) {
|
||||||
|
return _RoomRocketPagView(
|
||||||
|
resource: resource,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
loop: false,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
displayScale: 1,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
memoryTtl: _pagMemoryTtl,
|
||||||
|
imageReplacementUrlsByName: _topAvatarReplacementUrls(avatarUrl),
|
||||||
|
onCompleted: _complete,
|
||||||
|
defaultBuilder: (_) => const Center(child: SCRotatingDotsLoading()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,7 +264,7 @@ class _RoomRocketPagView extends StatefulWidget {
|
|||||||
required this.clipBehavior,
|
required this.clipBehavior,
|
||||||
required this.memoryTtl,
|
required this.memoryTtl,
|
||||||
this.underlayBuilder,
|
this.underlayBuilder,
|
||||||
this.onReady,
|
this.imageReplacementUrlsByName = const <String, String>{},
|
||||||
this.onCompleted,
|
this.onCompleted,
|
||||||
this.defaultBuilder,
|
this.defaultBuilder,
|
||||||
});
|
});
|
||||||
@ -250,7 +278,7 @@ class _RoomRocketPagView extends StatefulWidget {
|
|||||||
final Clip clipBehavior;
|
final Clip clipBehavior;
|
||||||
final Duration memoryTtl;
|
final Duration memoryTtl;
|
||||||
final WidgetBuilder? underlayBuilder;
|
final WidgetBuilder? underlayBuilder;
|
||||||
final ValueChanged<Size>? onReady;
|
final Map<String, String> imageReplacementUrlsByName;
|
||||||
final VoidCallback? onCompleted;
|
final VoidCallback? onCompleted;
|
||||||
final WidgetBuilder? defaultBuilder;
|
final WidgetBuilder? defaultBuilder;
|
||||||
|
|
||||||
@ -264,6 +292,9 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
final int _debugId = _nextDebugId++;
|
final int _debugId = _nextDebugId++;
|
||||||
late GlobalKey<PAGViewState> _pagKey;
|
late GlobalKey<PAGViewState> _pagKey;
|
||||||
Uint8List? _pagBytes;
|
Uint8List? _pagBytes;
|
||||||
|
String? _assetName;
|
||||||
|
Map<String, Uint8List>? _imageReplacementsByName;
|
||||||
|
late String _imageReplacementSignature;
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
bool _loadFailed = false;
|
bool _loadFailed = false;
|
||||||
bool _releasedFromMemory = false;
|
bool _releasedFromMemory = false;
|
||||||
@ -274,6 +305,9 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_pagKey = GlobalKey<PAGViewState>();
|
_pagKey = GlobalKey<PAGViewState>();
|
||||||
|
_imageReplacementSignature = _imageReplacementUrlsSignature(
|
||||||
|
widget.imageReplacementUrlsByName,
|
||||||
|
);
|
||||||
_debug('mount');
|
_debug('mount');
|
||||||
_prepareResource();
|
_prepareResource();
|
||||||
}
|
}
|
||||||
@ -281,9 +315,16 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant _RoomRocketPagView oldWidget) {
|
void didUpdateWidget(covariant _RoomRocketPagView oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.resource.trim() != widget.resource.trim()) {
|
final replacementSignature = _imageReplacementUrlsSignature(
|
||||||
|
widget.imageReplacementUrlsByName,
|
||||||
|
);
|
||||||
|
if (oldWidget.resource.trim() != widget.resource.trim() ||
|
||||||
|
replacementSignature != _imageReplacementSignature) {
|
||||||
_pagKey = GlobalKey<PAGViewState>();
|
_pagKey = GlobalKey<PAGViewState>();
|
||||||
_pagBytes = null;
|
_pagBytes = null;
|
||||||
|
_assetName = null;
|
||||||
|
_imageReplacementsByName = null;
|
||||||
|
_imageReplacementSignature = replacementSignature;
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
_loadFailed = false;
|
_loadFailed = false;
|
||||||
_releasedFromMemory = false;
|
_releasedFromMemory = false;
|
||||||
@ -327,23 +368,53 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
return _withUnderlay(underlay, defaultChild);
|
return _withUnderlay(underlay, defaultChild);
|
||||||
}
|
}
|
||||||
final bytes = _pagBytes;
|
final bytes = _pagBytes;
|
||||||
if (bytes == null) {
|
final assetName = _assetName;
|
||||||
|
final imageReplacementsReady =
|
||||||
|
!_hasImageReplacementUrls(widget.imageReplacementUrlsByName) ||
|
||||||
|
_imageReplacementsByName != null;
|
||||||
|
if ((bytes == null && assetName == null) || !imageReplacementsReady) {
|
||||||
return _withUnderlay(underlay, defaultChild);
|
return _withUnderlay(underlay, defaultChild);
|
||||||
}
|
}
|
||||||
final pagView = PAGView.bytes(
|
final imageReplacementsByName =
|
||||||
bytes,
|
_imageReplacementsByName ?? const <String, Uint8List>{};
|
||||||
key: _pagKey,
|
final useNativeAssetView =
|
||||||
width: null,
|
assetName != null && Platform.isIOS && widget.loop;
|
||||||
height: null,
|
final pagView =
|
||||||
autoPlay: false,
|
assetName != null
|
||||||
repeatCount: repeatCount,
|
? PAGView.asset(
|
||||||
onInit: _handleInit,
|
assetName,
|
||||||
onAnimationStart: _handleAnimationStart,
|
key: _pagKey,
|
||||||
onAnimationEnd: _handleAnimationEnd,
|
width: useNativeAssetView ? widget.width : null,
|
||||||
onAnimationCancel: _handleAnimationCancel,
|
height: useNativeAssetView ? widget.height : null,
|
||||||
onAnimationRepeat: _handleAnimationRepeat,
|
autoPlay: useNativeAssetView,
|
||||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
usePlatformView: useNativeAssetView,
|
||||||
);
|
repeatCount: repeatCount,
|
||||||
|
onInit: useNativeAssetView ? null : _handleInit,
|
||||||
|
onAnimationStart:
|
||||||
|
useNativeAssetView ? null : _handleAnimationStart,
|
||||||
|
onAnimationEnd: useNativeAssetView ? null : _handleAnimationEnd,
|
||||||
|
onAnimationCancel:
|
||||||
|
useNativeAssetView ? null : _handleAnimationCancel,
|
||||||
|
onAnimationRepeat:
|
||||||
|
useNativeAssetView ? null : _handleAnimationRepeat,
|
||||||
|
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||||
|
imageReplacementsByName: imageReplacementsByName,
|
||||||
|
)
|
||||||
|
: PAGView.bytes(
|
||||||
|
bytes,
|
||||||
|
key: _pagKey,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
autoPlay: false,
|
||||||
|
repeatCount: repeatCount,
|
||||||
|
onInit: _handleInit,
|
||||||
|
onAnimationStart: _handleAnimationStart,
|
||||||
|
onAnimationEnd: _handleAnimationEnd,
|
||||||
|
onAnimationCancel: _handleAnimationCancel,
|
||||||
|
onAnimationRepeat: _handleAnimationRepeat,
|
||||||
|
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||||
|
imageReplacementsByName: imageReplacementsByName,
|
||||||
|
);
|
||||||
final visibleUnderlay = _initialized ? null : underlay;
|
final visibleUnderlay = _initialized ? null : underlay;
|
||||||
final content = Stack(
|
final content = Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
@ -351,7 +422,10 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge,
|
widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge,
|
||||||
children: [
|
children: [
|
||||||
if (visibleUnderlay != null) visibleUnderlay,
|
if (visibleUnderlay != null) visibleUnderlay,
|
||||||
_scaledPagView(pagView),
|
if (useNativeAssetView)
|
||||||
|
Positioned.fill(child: pagView)
|
||||||
|
else
|
||||||
|
_scaledPagView(pagView),
|
||||||
if (!_initialized) Positioned.fill(child: defaultChild),
|
if (!_initialized) Positioned.fill(child: defaultChild),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -379,25 +453,68 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
final path = widget.resource.trim();
|
final path = widget.resource.trim();
|
||||||
_releasedFromMemory = false;
|
_releasedFromMemory = false;
|
||||||
_memoryReleaseTimer?.cancel();
|
_memoryReleaseTimer?.cancel();
|
||||||
|
_assetName = null;
|
||||||
|
_imageReplacementsByName = null;
|
||||||
_debug(
|
_debug(
|
||||||
'prepare kind=${_pagSourceKind(path)} '
|
'prepare kind=${_pagSourceKind(path)} '
|
||||||
'isPag=${RoomRocketPagEffectOverlay.isPag(path)}',
|
'isPag=${RoomRocketPagEffectOverlay.isPag(path)}',
|
||||||
);
|
);
|
||||||
_restartWatchdog();
|
_restartWatchdog();
|
||||||
unawaited(_loadBytes(path));
|
final assetName = _preferNativePagAsset(path);
|
||||||
|
final imageReplacementUrls = _validImageReplacementUrls(
|
||||||
|
widget.imageReplacementUrlsByName,
|
||||||
|
);
|
||||||
|
final imageReplacementSignature = _imageReplacementSignature;
|
||||||
|
if (assetName != null) {
|
||||||
|
_debug('use native asset loader asset=$assetName');
|
||||||
|
_assetName = assetName;
|
||||||
|
unawaited(
|
||||||
|
_loadImageReplacements(
|
||||||
|
path: path,
|
||||||
|
signature: imageReplacementSignature,
|
||||||
|
urlsByName: imageReplacementUrls,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (widget.loop) {
|
||||||
|
_startWatchdog?.cancel();
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unawaited(
|
||||||
|
_loadBytes(
|
||||||
|
path: path,
|
||||||
|
signature: imageReplacementSignature,
|
||||||
|
imageReplacementUrlsByName: imageReplacementUrls,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadBytes(String path) async {
|
Future<void> _loadBytes({
|
||||||
|
required String path,
|
||||||
|
required String signature,
|
||||||
|
required Map<String, String> imageReplacementUrlsByName,
|
||||||
|
}) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
_debug(
|
_debug(
|
||||||
'${_pagLoadAction(path)} start '
|
'${_pagLoadAction(path)} start '
|
||||||
'host=${_pagHost(path)}',
|
'host=${_pagHost(path)}',
|
||||||
);
|
);
|
||||||
|
final imageReplacementsFuture = _readImageReplacementsByName(
|
||||||
|
imageReplacementUrlsByName,
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
_debug('image replacements timeout');
|
||||||
|
return const <String, Uint8List>{};
|
||||||
|
},
|
||||||
|
);
|
||||||
final bytes = await _readPagBytes(
|
final bytes = await _readPagBytes(
|
||||||
path,
|
path,
|
||||||
).timeout(const Duration(seconds: 15));
|
).timeout(const Duration(seconds: 15));
|
||||||
if (!mounted || widget.resource.trim() != path) {
|
final imageReplacements = await imageReplacementsFuture;
|
||||||
|
if (!_isCurrentResource(path, signature)) {
|
||||||
_debug(
|
_debug(
|
||||||
'bytes ignored stale mounted=$mounted '
|
'bytes ignored stale mounted=$mounted '
|
||||||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||||||
@ -415,12 +532,13 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_pagBytes = bytes;
|
_pagBytes = bytes;
|
||||||
|
_imageReplacementsByName = imageReplacements;
|
||||||
_releasedFromMemory = false;
|
_releasedFromMemory = false;
|
||||||
});
|
});
|
||||||
_scheduleMemoryRelease(path);
|
_scheduleMemoryRelease(path);
|
||||||
_restartWatchdog();
|
_restartWatchdog();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!mounted || widget.resource.trim() != path) {
|
if (!_isCurrentResource(path, signature)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_markLoadFailed(
|
_markLoadFailed(
|
||||||
@ -429,6 +547,31 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadImageReplacements({
|
||||||
|
required String path,
|
||||||
|
required String signature,
|
||||||
|
required Map<String, String> urlsByName,
|
||||||
|
}) async {
|
||||||
|
if (urlsByName.isEmpty) {
|
||||||
|
_imageReplacementsByName = const <String, Uint8List>{};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final replacements = await _readImageReplacementsByName(
|
||||||
|
urlsByName,
|
||||||
|
).timeout(const Duration(seconds: 10));
|
||||||
|
if (!_isCurrentResource(path, signature)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _imageReplacementsByName = replacements);
|
||||||
|
} catch (error) {
|
||||||
|
_debug('image replacements failed: $error');
|
||||||
|
if (_isCurrentResource(path, signature)) {
|
||||||
|
setState(() => _imageReplacementsByName = const <String, Uint8List>{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleInit() {
|
void _handleInit() {
|
||||||
_debug('init');
|
_debug('init');
|
||||||
_startWatchdog?.cancel();
|
_startWatchdog?.cancel();
|
||||||
@ -452,7 +595,6 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
'viewSize=${widget.width?.toStringAsFixed(1)}x'
|
'viewSize=${widget.width?.toStringAsFixed(1)}x'
|
||||||
'${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}',
|
'${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}',
|
||||||
);
|
);
|
||||||
widget.onReady?.call(Size(rawWidth, rawHeight));
|
|
||||||
unawaited(_debugLayers(state, rawWidth, rawHeight));
|
unawaited(_debugLayers(state, rawWidth, rawHeight));
|
||||||
if (!_initialized) {
|
if (!_initialized) {
|
||||||
setState(() => _initialized = true);
|
setState(() => _initialized = true);
|
||||||
@ -541,6 +683,8 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_pagKey = GlobalKey<PAGViewState>();
|
_pagKey = GlobalKey<PAGViewState>();
|
||||||
_pagBytes = null;
|
_pagBytes = null;
|
||||||
|
_assetName = null;
|
||||||
|
_imageReplacementsByName = null;
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
_releasedFromMemory = true;
|
_releasedFromMemory = true;
|
||||||
});
|
});
|
||||||
@ -586,6 +730,35 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isCurrentResource(String path, String signature) {
|
||||||
|
return mounted &&
|
||||||
|
widget.resource.trim() == path &&
|
||||||
|
_imageReplacementSignature == signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Uint8List>> _readImageReplacementsByName(
|
||||||
|
Map<String, String> urlsByName,
|
||||||
|
) async {
|
||||||
|
if (urlsByName.isEmpty) {
|
||||||
|
return const <String, Uint8List>{};
|
||||||
|
}
|
||||||
|
final result = <String, Uint8List>{};
|
||||||
|
for (final entry in urlsByName.entries) {
|
||||||
|
try {
|
||||||
|
final bytes = await _readPagBytes(entry.value);
|
||||||
|
if (bytes.isNotEmpty) {
|
||||||
|
result[entry.key] = bytes;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
_debug(
|
||||||
|
'image replacement load failed layer=${entry.key} '
|
||||||
|
'resource=${_shortPagResource(entry.value)} error=$error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void _debug(String event) {
|
void _debug(String event) {
|
||||||
if (!kDebugMode) {
|
if (!kDebugMode) {
|
||||||
return;
|
return;
|
||||||
@ -675,6 +848,45 @@ String? _firstNonBlankPagAvatar(Iterable<String?> values) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> _topAvatarReplacementUrls(String? avatarUrl) {
|
||||||
|
final text = avatarUrl?.trim() ?? '';
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return const <String, String>{};
|
||||||
|
}
|
||||||
|
return <String, String>{_roomRocketTopAvatarLayerName: text};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasImageReplacementUrls(Map<String, String> urlsByName) {
|
||||||
|
return _validImageReplacementUrls(urlsByName).isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> _validImageReplacementUrls(Map<String, String> urlsByName) {
|
||||||
|
if (urlsByName.isEmpty) {
|
||||||
|
return const <String, String>{};
|
||||||
|
}
|
||||||
|
final result = <String, String>{};
|
||||||
|
urlsByName.forEach((key, value) {
|
||||||
|
final layerName = key.trim();
|
||||||
|
final resource = value.trim();
|
||||||
|
if (layerName.isNotEmpty && resource.isNotEmpty) {
|
||||||
|
result[layerName] = resource;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _imageReplacementUrlsSignature(Map<String, String> urlsByName) {
|
||||||
|
final entries =
|
||||||
|
_validImageReplacementUrls(urlsByName).entries.toList()
|
||||||
|
..sort((a, b) => a.key.compareTo(b.key));
|
||||||
|
if (entries.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
.map((entry) => '${entry.key}\u0000${entry.value}')
|
||||||
|
.join('\u0001');
|
||||||
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
String debugNormalizeRoomRocketPagResource(String resource) {
|
String debugNormalizeRoomRocketPagResource(String resource) {
|
||||||
return _normalizePagResource(resource);
|
return _normalizePagResource(resource);
|
||||||
@ -752,6 +964,22 @@ String _pagLoadAction(String value) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _preferNativePagAsset(String value) {
|
||||||
|
if (!Platform.isIOS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final normalized = _normalizePagResource(value);
|
||||||
|
if (!RoomRocketPagEffectOverlay.isPag(normalized) ||
|
||||||
|
_pagNetworkUri(normalized) != null ||
|
||||||
|
_pagFilePath(normalized) != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('sc_images/') || normalized.startsWith('assets/')) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String _normalizePagResource(String value) {
|
String _normalizePagResource(String value) {
|
||||||
final normalized = normalizeImageResourceUrl(value.trim());
|
final normalized = normalizeImageResourceUrl(value.trim());
|
||||||
return _pagNetworkUri(normalized)?.toString() ?? normalized;
|
return _pagNetworkUri(normalized)?.toString() ?? normalized;
|
||||||
|
|||||||
@ -8,6 +8,26 @@ import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
|||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
|
|
||||||
|
void showRoomGameListSheet(BuildContext context, {String? initialGameId}) {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final guardLease = SCRoomTopLayerGuard().acquire(
|
||||||
|
reason: 'room_game_list_sheet',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
unawaited(
|
||||||
|
showBottomInBottomDialog(
|
||||||
|
context,
|
||||||
|
RoomGameBottomSheet(roomContext: context, initialGameId: initialGameId),
|
||||||
|
barrierColor: Colors.black54,
|
||||||
|
).whenComplete(guardLease.release),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
guardLease.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RoomGameEntryButton extends StatelessWidget {
|
class RoomGameEntryButton extends StatelessWidget {
|
||||||
const RoomGameEntryButton({super.key});
|
const RoomGameEntryButton({super.key});
|
||||||
|
|
||||||
@ -15,20 +35,7 @@ class RoomGameEntryButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SCDebounceWidget(
|
return SCDebounceWidget(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final guardLease = SCRoomTopLayerGuard().acquire(
|
showRoomGameListSheet(context);
|
||||||
reason: 'room_game_list_sheet',
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
unawaited(
|
|
||||||
showBottomInBottomDialog(
|
|
||||||
context,
|
|
||||||
RoomGameBottomSheet(roomContext: context),
|
|
||||||
barrierColor: Colors.black54,
|
|
||||||
).whenComplete(guardLease.release),
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
guardLease.release();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: SCSvgaAssetWidget(
|
child: SCSvgaAssetWidget(
|
||||||
assetPath: "sc_images/room/sc_icon_room_game_entry_anim.svga",
|
assetPath: "sc_images/room/sc_icon_room_game_entry_anim.svga",
|
||||||
|
|||||||
@ -669,8 +669,9 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
return Container(
|
return Container(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(minHeight: 49.w, maxWidth: 315.w),
|
width: 315.w,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 6.w),
|
constraints: BoxConstraints(minHeight: 49.w),
|
||||||
|
padding: EdgeInsetsDirectional.fromSTEB(28.w, 6.w, 18.w, 6.w),
|
||||||
margin: EdgeInsets.symmetric(horizontal: 10.w).copyWith(bottom: 8.w),
|
margin: EdgeInsets.symmetric(horizontal: 10.w).copyWith(bottom: 8.w),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
@ -679,18 +680,21 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
centerSlice: _cpSystemNoticeBubbleCenterSlice,
|
centerSlice: _cpSystemNoticeBubbleCenterSlice,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Align(
|
||||||
widget.msg.msg ?? '',
|
alignment: Alignment.center,
|
||||||
maxLines: 4,
|
child: Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
widget.msg.msg ?? '',
|
||||||
style: TextStyle(
|
maxLines: 4,
|
||||||
fontSize: sp(12),
|
overflow: TextOverflow.ellipsis,
|
||||||
height: 1.35,
|
style: TextStyle(
|
||||||
color: Color(_msgColor(widget.msg.type ?? "")),
|
fontSize: sp(12),
|
||||||
fontWeight: FontWeight.w500,
|
height: 1.35,
|
||||||
decoration: TextDecoration.none,
|
color: Color(_msgColor(widget.msg.type ?? "")),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.left,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
|||||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
|
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
@ -751,7 +752,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
currentProfile?.id,
|
currentProfile?.id,
|
||||||
]),
|
]),
|
||||||
rightUserId: _firstNonBlank([partner.userId]),
|
rightUserId: _firstNonBlank([partner.userId]),
|
||||||
days: _firstNonBlank([cp?.days, "0"]),
|
days: scCpRelationDisplayDays(_firstNonBlank([cp?.days, "0"])),
|
||||||
relationType: _normalizeCpRelationType(cp?.relationType),
|
relationType: _normalizeCpRelationType(cp?.relationType),
|
||||||
dismissCost: cp?.dismissCost,
|
dismissCost: cp?.dismissCost,
|
||||||
cpValue: cp?.cpValue ?? 0,
|
cpValue: cp?.cpValue ?? 0,
|
||||||
@ -1638,7 +1639,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
child: text(
|
child: text(
|
||||||
SCAppLocalizations.of(
|
SCAppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
)!.timeSpentTogether(item.days ?? ""),
|
)!.timeSpentTogether(scCpRelationDisplayDays(item.days)),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
textColor: Color(0xFFFF79A1),
|
textColor: Color(0xFFFF79A1),
|
||||||
@ -1830,7 +1831,9 @@ class _RoomCpProgressData {
|
|||||||
currentValue: currentValue,
|
currentValue: currentValue,
|
||||||
targetValue: currentValue > 0 ? currentValue : 1,
|
targetValue: currentValue > 0 ? currentValue : 1,
|
||||||
expAwayText: SCStringUtils.formatCompactNumber(0),
|
expAwayText: SCStringUtils.formatCompactNumber(0),
|
||||||
nextLevelText: _levelShortText(_levelIndex(cp.levelText)),
|
nextLevelText: _levelShortText(
|
||||||
|
_nextLevelIndex(_levelIndex(cp.levelText)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1900,7 +1903,9 @@ class _RoomCpProgressData {
|
|||||||
nextLevelText:
|
nextLevelText:
|
||||||
nextLevel == null
|
nextLevel == null
|
||||||
? _levelShortText(
|
? _levelShortText(
|
||||||
currentLevel?.level ?? _levelIndex(cp.levelText),
|
_nextLevelIndex(
|
||||||
|
currentLevel?.level ?? _levelIndex(cp.levelText),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: _levelShortText(nextLevel.level, fallback: nextLevel.levelName),
|
: _levelShortText(nextLevel.level, fallback: nextLevel.levelName),
|
||||||
);
|
);
|
||||||
@ -1941,13 +1946,13 @@ class _RoomCpProgressData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String _levelShortText(int? level, {String? fallback}) {
|
static String _levelShortText(int? level, {String? fallback}) {
|
||||||
|
if (level != null) {
|
||||||
|
return "Lv. $level";
|
||||||
|
}
|
||||||
final fallbackText = fallback?.trim() ?? "";
|
final fallbackText = fallback?.trim() ?? "";
|
||||||
if (fallbackText.isNotEmpty) {
|
if (fallbackText.isNotEmpty) {
|
||||||
return fallbackText;
|
return fallbackText;
|
||||||
}
|
}
|
||||||
if (level != null) {
|
|
||||||
return "Lv. $level";
|
|
||||||
}
|
|
||||||
return "Lv. 1";
|
return "Lv. 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1962,6 +1967,10 @@ class _RoomCpProgressData {
|
|||||||
) ??
|
) ??
|
||||||
1;
|
1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int _nextLevelIndex(int level) {
|
||||||
|
return (level + 1).clamp(1, 5).toInt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CpProgressLevel {
|
class _CpProgressLevel {
|
||||||
|
|||||||
228
lib/ui_kit/widgets/room/seat/room_mic_relation_effect.dart
Normal file
228
lib/ui_kit/widgets/room/seat/room_mic_relation_effect.dart
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
|
|
||||||
|
class RoomMicRelationEffect extends StatefulWidget {
|
||||||
|
const RoomMicRelationEffect({
|
||||||
|
super.key,
|
||||||
|
required this.relationType,
|
||||||
|
this.cpLevel,
|
||||||
|
this.width = 108,
|
||||||
|
this.height = 54,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String assetBase = "sc_images/room/mic";
|
||||||
|
|
||||||
|
final String relationType;
|
||||||
|
final int? cpLevel;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RoomMicRelationEffect> createState() => _RoomMicRelationEffectState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RoomMicRelationEffectState extends State<RoomMicRelationEffect> {
|
||||||
|
static const Duration _cycleDuration = Duration(seconds: 4);
|
||||||
|
static const Duration _pagMemoryTtl = Duration(hours: 1);
|
||||||
|
|
||||||
|
Timer? _cycleTimer;
|
||||||
|
int _assetIndex = 0;
|
||||||
|
List<_RoomMicRelationAsset> _assets = const [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_syncAssets(restart: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant RoomMicRelationEffect oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.relationType != widget.relationType ||
|
||||||
|
oldWidget.cpLevel != widget.cpLevel) {
|
||||||
|
_syncAssets(restart: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_syncAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_cycleTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_assets.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final asset = _assets[_assetIndex % _assets.length];
|
||||||
|
final width = widget.width.w;
|
||||||
|
final height = widget.height.w;
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: _buildAsset(asset, width: width, height: height),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAsset(
|
||||||
|
_RoomMicRelationAsset asset, {
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
}) {
|
||||||
|
switch (asset.kind) {
|
||||||
|
case _RoomMicRelationAssetKind.svga:
|
||||||
|
return SCSvgaAssetWidget(
|
||||||
|
assetPath: asset.path,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
active: true,
|
||||||
|
loop: true,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
allowDrawingOverflow: true,
|
||||||
|
);
|
||||||
|
case _RoomMicRelationAssetKind.pag:
|
||||||
|
return RoomRocketPagPreview(
|
||||||
|
resource: asset.path,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
loop: true,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
memoryTtl: _pagMemoryTtl,
|
||||||
|
defaultBuilder: (_) => SizedBox(width: width, height: height),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncAssets({bool restart = false}) {
|
||||||
|
final nextAssets = _assetsForRelation(
|
||||||
|
widget.relationType,
|
||||||
|
cpLevel: widget.cpLevel,
|
||||||
|
);
|
||||||
|
final changed = !_sameAssetList(_assets, nextAssets);
|
||||||
|
if (!changed && !restart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cycleTimer?.cancel();
|
||||||
|
_assets = nextAssets;
|
||||||
|
_assetIndex = 0;
|
||||||
|
if (mounted && changed) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
if (_assets.length > 1) {
|
||||||
|
_cycleTimer = Timer.periodic(_cycleDuration, (_) {
|
||||||
|
if (!mounted || _assets.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _assetIndex = (_assetIndex + 1) % _assets.length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _sameAssetList(
|
||||||
|
List<_RoomMicRelationAsset> left,
|
||||||
|
List<_RoomMicRelationAsset> right,
|
||||||
|
) {
|
||||||
|
if (left.length != right.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var index = 0; index < left.length; index += 1) {
|
||||||
|
if (left[index] != right[index]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_RoomMicRelationAsset> _assetsForRelation(
|
||||||
|
String relationType, {
|
||||||
|
required int? cpLevel,
|
||||||
|
}) {
|
||||||
|
switch (scNormalizeCpRelationType(relationType)) {
|
||||||
|
case "BROTHER":
|
||||||
|
return const [
|
||||||
|
_RoomMicRelationAsset.pag(
|
||||||
|
"${RoomMicRelationEffect.assetBase}/brother_mic.pag",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case "SISTERS":
|
||||||
|
return const [
|
||||||
|
_RoomMicRelationAsset.pag(
|
||||||
|
"${RoomMicRelationEffect.assetBase}/sisters_mic.pag",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return _cpAssetsForLevel(cpLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_RoomMicRelationAsset> _cpAssetsForLevel(int? cpLevel) {
|
||||||
|
final level = (cpLevel ?? 1).clamp(1, 5).toInt();
|
||||||
|
switch (level) {
|
||||||
|
case 2:
|
||||||
|
return const [
|
||||||
|
_RoomMicRelationAsset.svga(
|
||||||
|
"${RoomMicRelationEffect.assetBase}/CP_mic_2.svga",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case 3:
|
||||||
|
return const [
|
||||||
|
_RoomMicRelationAsset.svga(
|
||||||
|
"${RoomMicRelationEffect.assetBase}/CP_mic_3.svga",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case 4:
|
||||||
|
return const [
|
||||||
|
_RoomMicRelationAsset.pag(
|
||||||
|
"${RoomMicRelationEffect.assetBase}/CP_mic_4.pag",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case 5:
|
||||||
|
return const [
|
||||||
|
_RoomMicRelationAsset.pag(
|
||||||
|
"${RoomMicRelationEffect.assetBase}/CP_mic_5.pag",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
return const [
|
||||||
|
_RoomMicRelationAsset.pag(
|
||||||
|
"${RoomMicRelationEffect.assetBase}/CP_mic_1.pag",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _RoomMicRelationAssetKind { svga, pag }
|
||||||
|
|
||||||
|
class _RoomMicRelationAsset {
|
||||||
|
const _RoomMicRelationAsset._({required this.path, required this.kind});
|
||||||
|
|
||||||
|
const _RoomMicRelationAsset.svga(String path)
|
||||||
|
: this._(path: path, kind: _RoomMicRelationAssetKind.svga);
|
||||||
|
|
||||||
|
const _RoomMicRelationAsset.pag(String path)
|
||||||
|
: this._(path: path, kind: _RoomMicRelationAssetKind.pag);
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
final _RoomMicRelationAssetKind kind;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is _RoomMicRelationAsset &&
|
||||||
|
other.path == path &&
|
||||||
|
other.kind == kind;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(path, kind);
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_pair_utils.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/seat/room_mic_relation_effect.dart';
|
||||||
|
|
||||||
import '../../../../modules/room/seat/sc_seat_item.dart';
|
import '../../../../modules/room/seat/sc_seat_item.dart';
|
||||||
|
|
||||||
@ -14,6 +16,10 @@ class RoomSeatWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
||||||
static const int _defaultVoiceRoomSeatCount = 10;
|
static const int _defaultVoiceRoomSeatCount = 10;
|
||||||
|
static const int _seatColumnCount = 5;
|
||||||
|
static const double _relationEffectWidth = 108;
|
||||||
|
static const double _relationEffectHeight = 54;
|
||||||
|
static const double _relationEffectTop = -2;
|
||||||
|
|
||||||
int _lastSeatCount = 0;
|
int _lastSeatCount = 0;
|
||||||
|
|
||||||
@ -84,6 +90,7 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
|||||||
user?.getHeaddress()?.sourceUrl ?? "",
|
user?.getHeaddress()?.sourceUrl ?? "",
|
||||||
user?.getHeaddress()?.cover ?? "",
|
user?.getHeaddress()?.cover ?? "",
|
||||||
user?.getVIP()?.name ?? "",
|
user?.getVIP()?.name ?? "",
|
||||||
|
scCpProfileRelationVersion(user),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return version;
|
return version;
|
||||||
@ -96,6 +103,7 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
|||||||
(context, provider) => _RoomSeatLayoutSnapshot(
|
(context, provider) => _RoomSeatLayoutSnapshot(
|
||||||
seatCount: _seatCountFromMicIndexes(provider.roomWheatMap.keys),
|
seatCount: _seatCountFromMicIndexes(provider.roomWheatMap.keys),
|
||||||
seatContentVersion: _seatContentVersion(provider),
|
seatContentVersion: _seatContentVersion(provider),
|
||||||
|
showRoomVisualEffects: provider.shouldShowRoomVisualEffects,
|
||||||
configuredSeatCount:
|
configuredSeatCount:
|
||||||
provider.currenRoom?.roomProfile?.roomSetting?.mikeSize
|
provider.currenRoom?.roomProfile?.roomSetting?.mikeSize
|
||||||
?.toInt() ??
|
?.toInt() ??
|
||||||
@ -126,15 +134,7 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
_buildSeatRow(const [0, 1, 2, 3, 4]),
|
||||||
children: [
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 0)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 1)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 2)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 3)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 4)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -146,25 +146,8 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
_buildSeatRow(const [0, 1, 2, 3, 4]),
|
||||||
children: [
|
_buildSeatRow(const [5, 6, 7, 8, 9]),
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 0)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 1)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 2)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 3)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 4)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 5)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 6)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 7)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 8)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 9)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -176,33 +159,9 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
_buildSeatRow(const [0, 1, 2, 3, 4]),
|
||||||
children: [
|
_buildSeatRow(const [5, 6, 7, 8, 9]),
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 0)),
|
_buildSeatRow(const [10, 11, 12, 13, 14]),
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 1)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 2)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 3)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 4)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 5)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 6)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 7)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 8)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 9)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 10)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 11)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 12)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 13)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 14)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -214,52 +173,96 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
_buildSeatRow(const [0, 1, 2, 3, 4]),
|
||||||
children: [
|
_buildSeatRow(const [5, 6, 7, 8, 9]),
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 0)),
|
_buildSeatRow(const [10, 11, 12, 13, 14]),
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 1)),
|
_buildSeatRow(const [15, 16, 17, 18, 19]),
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 2)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 3)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 4)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 5)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 6)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 7)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 8)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 9)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 10)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 11)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 12)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 13)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 14)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 15)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 16)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 17)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 18)),
|
|
||||||
Expanded(flex: 1, child: SCSeatItem(index: 19)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSeatRow(List<int> indexes) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final rowWidth = constraints.maxWidth;
|
||||||
|
final columnWidth = rowWidth / _seatColumnCount;
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
for (final index in indexes)
|
||||||
|
Expanded(flex: 1, child: SCSeatItem(index: index)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (rowWidth.isFinite)
|
||||||
|
..._buildRelationEffectsForRow(
|
||||||
|
context,
|
||||||
|
indexes: indexes,
|
||||||
|
columnWidth: columnWidth,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildRelationEffectsForRow(
|
||||||
|
BuildContext context, {
|
||||||
|
required List<int> indexes,
|
||||||
|
required double columnWidth,
|
||||||
|
}) {
|
||||||
|
final provider = context.read<RtcProvider>();
|
||||||
|
if (!provider.shouldShowRoomVisualEffects) {
|
||||||
|
return const <Widget>[];
|
||||||
|
}
|
||||||
|
final effects = <Widget>[];
|
||||||
|
for (var slot = 0; slot < indexes.length - 1; slot += 1) {
|
||||||
|
final relationInfo = _relationInfoBetweenSeats(
|
||||||
|
provider,
|
||||||
|
indexes[slot],
|
||||||
|
indexes[slot + 1],
|
||||||
|
);
|
||||||
|
if (relationInfo == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final left = ((slot + 1) * columnWidth - (_relationEffectWidth.w / 2))
|
||||||
|
.clamp(0.0, double.infinity);
|
||||||
|
effects.add(
|
||||||
|
Positioned(
|
||||||
|
left: left,
|
||||||
|
top: _relationEffectTop.w,
|
||||||
|
width: _relationEffectWidth.w,
|
||||||
|
height: _relationEffectHeight.w,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: RoomMicRelationEffect(
|
||||||
|
relationType: relationInfo.relationType,
|
||||||
|
cpLevel: relationInfo.cpLevel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return effects;
|
||||||
|
}
|
||||||
|
|
||||||
|
SCCpRelationPairInfo? _relationInfoBetweenSeats(
|
||||||
|
RtcProvider provider,
|
||||||
|
int leftIndex,
|
||||||
|
int rightIndex,
|
||||||
|
) {
|
||||||
|
final leftUser = provider.micAtIndexForDisplay(leftIndex)?.user;
|
||||||
|
final rightUser = provider.micAtIndexForDisplay(rightIndex)?.user;
|
||||||
|
return scCpRelationInfoBetweenProfiles(leftUser, rightUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RoomSeatLayoutSnapshot {
|
class _RoomSeatLayoutSnapshot {
|
||||||
const _RoomSeatLayoutSnapshot({
|
const _RoomSeatLayoutSnapshot({
|
||||||
required this.seatCount,
|
required this.seatCount,
|
||||||
required this.seatContentVersion,
|
required this.seatContentVersion,
|
||||||
|
required this.showRoomVisualEffects,
|
||||||
required this.configuredSeatCount,
|
required this.configuredSeatCount,
|
||||||
required this.hasCurrentRoom,
|
required this.hasCurrentRoom,
|
||||||
required this.isExitingCurrentVoiceRoomSession,
|
required this.isExitingCurrentVoiceRoomSession,
|
||||||
@ -267,6 +270,7 @@ class _RoomSeatLayoutSnapshot {
|
|||||||
|
|
||||||
final int seatCount;
|
final int seatCount;
|
||||||
final int seatContentVersion;
|
final int seatContentVersion;
|
||||||
|
final bool showRoomVisualEffects;
|
||||||
final int configuredSeatCount;
|
final int configuredSeatCount;
|
||||||
final bool hasCurrentRoom;
|
final bool hasCurrentRoom;
|
||||||
final bool isExitingCurrentVoiceRoomSession;
|
final bool isExitingCurrentVoiceRoomSession;
|
||||||
@ -279,6 +283,7 @@ class _RoomSeatLayoutSnapshot {
|
|||||||
return other is _RoomSeatLayoutSnapshot &&
|
return other is _RoomSeatLayoutSnapshot &&
|
||||||
other.seatCount == seatCount &&
|
other.seatCount == seatCount &&
|
||||||
other.seatContentVersion == seatContentVersion &&
|
other.seatContentVersion == seatContentVersion &&
|
||||||
|
other.showRoomVisualEffects == showRoomVisualEffects &&
|
||||||
other.configuredSeatCount == configuredSeatCount &&
|
other.configuredSeatCount == configuredSeatCount &&
|
||||||
other.hasCurrentRoom == hasCurrentRoom &&
|
other.hasCurrentRoom == hasCurrentRoom &&
|
||||||
other.isExitingCurrentVoiceRoomSession ==
|
other.isExitingCurrentVoiceRoomSession ==
|
||||||
@ -289,6 +294,7 @@ class _RoomSeatLayoutSnapshot {
|
|||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
seatCount,
|
seatCount,
|
||||||
seatContentVersion,
|
seatContentVersion,
|
||||||
|
showRoomVisualEffects,
|
||||||
configuredSeatCount,
|
configuredSeatCount,
|
||||||
hasCurrentRoom,
|
hasCurrentRoom,
|
||||||
isExitingCurrentVoiceRoomSession,
|
isExitingCurrentVoiceRoomSession,
|
||||||
|
|||||||
@ -10,13 +10,16 @@ import android.view.Surface;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.libpag.PAGFile;
|
import org.libpag.PAGFile;
|
||||||
|
import org.libpag.PAGImage;
|
||||||
import org.libpag.PAGLayer;
|
import org.libpag.PAGLayer;
|
||||||
|
import org.libpag.PAGScaleMode;
|
||||||
import org.libpag.PAGSurface;
|
import org.libpag.PAGSurface;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
@ -79,6 +82,7 @@ public class FlutterPagPlugin implements FlutterPlugin, MethodCallHandler {
|
|||||||
final static String _argumentCacheEnabled = "cacheEnabled";
|
final static String _argumentCacheEnabled = "cacheEnabled";
|
||||||
final static String _argumentCacheSize = "cacheSize";
|
final static String _argumentCacheSize = "cacheSize";
|
||||||
final static String _argumentMultiThreadEnabled = "multiThreadEnabled";
|
final static String _argumentMultiThreadEnabled = "multiThreadEnabled";
|
||||||
|
final static String _argumentImageReplacementsByName = "imageReplacementsByName";
|
||||||
|
|
||||||
// 回调
|
// 回调
|
||||||
final static String _playCallback = "PAGCallback";
|
final static String _playCallback = "PAGCallback";
|
||||||
@ -224,6 +228,7 @@ public class FlutterPagPlugin implements FlutterPlugin, MethodCallHandler {
|
|||||||
result.error("-1100", "load composition is null! ", null);
|
result.error("-1100", "load composition is null! ", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
applyImageReplacementsByName(composition, call);
|
||||||
|
|
||||||
final int repeatCount = call.argument(_argumentRepeatCount);
|
final int repeatCount = call.argument(_argumentRepeatCount);
|
||||||
final double initProgress = call.argument(_argumentInitProgress);
|
final double initProgress = call.argument(_argumentInitProgress);
|
||||||
@ -276,6 +281,34 @@ public class FlutterPagPlugin implements FlutterPlugin, MethodCallHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyImageReplacementsByName(PAGFile composition, MethodCall call) {
|
||||||
|
Map<String, Object> replacements = call.argument(_argumentImageReplacementsByName);
|
||||||
|
if (composition == null || replacements == null || replacements.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Object> entry : replacements.entrySet()) {
|
||||||
|
String layerName = entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (layerName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
layerName = layerName.trim();
|
||||||
|
if (layerName.isEmpty() || !(value instanceof byte[])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byte[] bytes = (byte[]) value;
|
||||||
|
if (bytes.length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PAGImage image = PAGImage.FromBytes(bytes);
|
||||||
|
if (image == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
image.setScaleMode(PAGScaleMode.Zoom);
|
||||||
|
composition.replaceImageByName(layerName, image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void start(MethodCall call) {
|
void start(MethodCall call) {
|
||||||
FlutterPagPlayer flutterPagPlayer = getFlutterPagPlayer(call);
|
FlutterPagPlayer flutterPagPlayer = getFlutterPagPlayer(call);
|
||||||
if (flutterPagPlayer != null) {
|
if (flutterPagPlayer != null) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#import "FlutterPagPlugin.h"
|
#import "FlutterPagPlugin.h"
|
||||||
#import "TGFlutterPagRender.h"
|
#import "TGFlutterPagRender.h"
|
||||||
#import "TGFlutterPagDownloadManager.h"
|
#import "TGFlutterPagDownloadManager.h"
|
||||||
|
#import "TGFlutterPagPlatformView.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
FlutterPagPlugin,处理flutter MethodChannel约定的方法
|
FlutterPagPlugin,处理flutter MethodChannel约定的方法
|
||||||
@ -33,6 +34,15 @@
|
|||||||
/// 用于通信的channel
|
/// 用于通信的channel
|
||||||
@property (nonatomic, strong)FlutterMethodChannel* channel;
|
@property (nonatomic, strong)FlutterMethodChannel* channel;
|
||||||
|
|
||||||
|
- (NSDictionary<NSString*, NSData*>*)imageReplacementsByNameFromArguments:(id)arguments;
|
||||||
|
|
||||||
|
- (void)pagRenderWithPagData:(NSData*)pagData
|
||||||
|
progress:(double)progress
|
||||||
|
repeatCount:(int)repeatCount
|
||||||
|
autoPlay:(BOOL)autoPlay
|
||||||
|
imageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName
|
||||||
|
result:(FlutterResult)result;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation FlutterPagPlugin
|
@implementation FlutterPagPlugin
|
||||||
@ -45,6 +55,8 @@
|
|||||||
instance.registrar = registrar;
|
instance.registrar = registrar;
|
||||||
instance.channel = channel;
|
instance.channel = channel;
|
||||||
[registrar addMethodCallDelegate:instance channel:channel];
|
[registrar addMethodCallDelegate:instance channel:channel];
|
||||||
|
[registrar registerViewFactory:[[TGFlutterPagPlatformViewFactory alloc] initWithRegistrar:registrar]
|
||||||
|
withId:@"flutter_pag_plugin_view"];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)getLayersUnderPoint:(id)arguments result:(FlutterResult _Nonnull)result {
|
- (void)getLayersUnderPoint:(id)arguments result:(FlutterResult _Nonnull)result {
|
||||||
@ -136,6 +148,7 @@
|
|||||||
if(arguments[@"autoPlay"]){
|
if(arguments[@"autoPlay"]){
|
||||||
autoPlay = [[arguments objectForKey:@"autoPlay"] boolValue];
|
autoPlay = [[arguments objectForKey:@"autoPlay"] boolValue];
|
||||||
}
|
}
|
||||||
|
NSDictionary<NSString*, NSData*>* imageReplacementsByName = [self imageReplacementsByNameFromArguments:arguments];
|
||||||
|
|
||||||
NSString* assetName = arguments[@"assetName"];
|
NSString* assetName = arguments[@"assetName"];
|
||||||
NSData *pagData = nil;
|
NSData *pagData = nil;
|
||||||
@ -157,26 +170,24 @@
|
|||||||
[self setCacheData:key data:pagData];
|
[self setCacheData:key data:pagData];
|
||||||
|
|
||||||
}
|
}
|
||||||
[self pagRenderWithPagData:pagData progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result];
|
[self pagRenderWithPagData:pagData progress:initProgress repeatCount:repeatCount autoPlay:autoPlay imageReplacementsByName:imageReplacementsByName result:result];
|
||||||
}
|
}
|
||||||
NSString* url = arguments[@"url"];
|
NSString* url = arguments[@"url"];
|
||||||
if ([url isKindOfClass:NSString.class] && url.length > 0) {
|
if ([url isKindOfClass:NSString.class] && url.length > 0) {
|
||||||
NSURLSessionDownloadTask *task;
|
|
||||||
[task resume];
|
|
||||||
NSString *key = url;
|
NSString *key = url;
|
||||||
pagData = [self getCacheData:key];
|
pagData = [self getCacheData:key];
|
||||||
if (!pagData) {
|
if (!pagData) {
|
||||||
__weak typeof(self) weak_self = self;
|
__weak typeof(self) weak_self = self;
|
||||||
[TGFlutterPagDownloadManager download:url completionHandler:^(NSData * _Nonnull data, NSError * _Nonnull error) {
|
[TGFlutterPagDownloadManager download:url completionHandler:^(NSData * _Nonnull data, NSError * _Nonnull error) {
|
||||||
if (data) {
|
if (data) {
|
||||||
[weak_self setCacheData:key data:pagData];
|
[weak_self setCacheData:key data:data];
|
||||||
[weak_self pagRenderWithPagData:data progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result];
|
[weak_self pagRenderWithPagData:data progress:initProgress repeatCount:repeatCount autoPlay:autoPlay imageReplacementsByName:imageReplacementsByName result:result];
|
||||||
}else{
|
}else{
|
||||||
result(@-1);
|
result(@-1);
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
}else{
|
}else{
|
||||||
[self pagRenderWithPagData:pagData progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result];
|
[self pagRenderWithPagData:pagData progress:initProgress repeatCount:repeatCount autoPlay:autoPlay imageReplacementsByName:imageReplacementsByName result:result];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,13 +195,47 @@
|
|||||||
if(bytesData != nil && [bytesData isKindOfClass:FlutterStandardTypedData.class]){
|
if(bytesData != nil && [bytesData isKindOfClass:FlutterStandardTypedData.class]){
|
||||||
FlutterStandardTypedData *typedData = bytesData;
|
FlutterStandardTypedData *typedData = bytesData;
|
||||||
if(typedData.type == FlutterStandardDataTypeUInt8 && typedData.data != nil){
|
if(typedData.type == FlutterStandardDataTypeUInt8 && typedData.data != nil){
|
||||||
[self pagRenderWithPagData:typedData.data progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result];
|
[self pagRenderWithPagData:typedData.data progress:initProgress repeatCount:repeatCount autoPlay:autoPlay imageReplacementsByName:imageReplacementsByName result:result];
|
||||||
}else{
|
}else{
|
||||||
result(@-1);
|
result(@-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSDictionary<NSString*, NSData*>*)imageReplacementsByNameFromArguments:(id)arguments {
|
||||||
|
if (![arguments isKindOfClass:NSDictionary.class]) {
|
||||||
|
return @{};
|
||||||
|
}
|
||||||
|
NSDictionary* params = arguments;
|
||||||
|
id raw = params[@"imageReplacementsByName"];
|
||||||
|
if (![raw isKindOfClass:NSDictionary.class]) {
|
||||||
|
return @{};
|
||||||
|
}
|
||||||
|
NSMutableDictionary<NSString*, NSData*>* result = [[NSMutableDictionary alloc] init];
|
||||||
|
[(NSDictionary*)raw enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL* stop) {
|
||||||
|
if (![key isKindOfClass:NSString.class]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString* layerName = [(NSString*)key stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||||
|
if (layerName.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSData* data = nil;
|
||||||
|
if ([value isKindOfClass:FlutterStandardTypedData.class]) {
|
||||||
|
FlutterStandardTypedData* typedData = value;
|
||||||
|
if (typedData.type == FlutterStandardDataTypeUInt8) {
|
||||||
|
data = typedData.data;
|
||||||
|
}
|
||||||
|
} else if ([value isKindOfClass:NSData.class]) {
|
||||||
|
data = value;
|
||||||
|
}
|
||||||
|
if (data.length > 0) {
|
||||||
|
result[layerName] = data;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
||||||
|
|
||||||
id arguments = call.arguments;
|
id arguments = call.arguments;
|
||||||
@ -215,21 +260,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)pagRenderWithPagData:(NSData *)pagData progress:(double)progress repeatCount:(int)repeatCount autoPlay:(BOOL)autoPlay result:(FlutterResult)result{
|
-(void)pagRenderWithPagData:(NSData *)pagData progress:(double)progress repeatCount:(int)repeatCount autoPlay:(BOOL)autoPlay imageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName result:(FlutterResult)result{
|
||||||
|
if (pagData == nil || pagData.length == 0) {
|
||||||
|
result(@-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
__block int64_t textureId = -1;
|
__block int64_t textureId = -1;
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
TGFlutterPagRender *render = [[TGFlutterPagRender alloc] initWithPagData:pagData progress:progress frameUpdateCallback:^{
|
TGFlutterPagRender *render = [[TGFlutterPagRender alloc] initWithPagData:pagData progress:progress imageReplacementsByName:imageReplacementsByName frameUpdateCallback:^{
|
||||||
[weakSelf.textures textureFrameAvailable:textureId];
|
[weakSelf.textures textureFrameAvailable:textureId];
|
||||||
} eventCallback:^(NSString * event) {
|
} eventCallback:^(NSString * event) {
|
||||||
[weakSelf.channel invokeMethod:PlayCallback arguments:@{ArgumentTextureId:@(textureId), ArgumentEvent:event}];
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[weakSelf.channel invokeMethod:PlayCallback arguments:@{ArgumentTextureId:@(textureId), ArgumentEvent:event}];
|
||||||
|
});
|
||||||
}];
|
}];
|
||||||
|
CGSize renderSize = [render size];
|
||||||
|
if (render == nil || renderSize.width <= 0 || renderSize.height <= 0) {
|
||||||
|
result(@-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
[render setRepeatCount:repeatCount];
|
[render setRepeatCount:repeatCount];
|
||||||
textureId = [self.textures registerTexture:render];
|
textureId = [self.textures registerTexture:render];
|
||||||
if(_renderMap == nil){
|
if(_renderMap == nil){
|
||||||
_renderMap = [[NSMutableDictionary alloc] init];
|
_renderMap = [[NSMutableDictionary alloc] init];
|
||||||
}
|
}
|
||||||
[_renderMap setObject:render forKey:@(textureId)];
|
[_renderMap setObject:render forKey:@(textureId)];
|
||||||
result(@{@"textureId":@(textureId), @"width":@([render size].width), @"height":@([render size].height)});
|
result(@{@"textureId":@(textureId), @"width":@(renderSize.width), @"height":@(renderSize.height)});
|
||||||
if(autoPlay){
|
if(autoPlay){
|
||||||
[render startRender];
|
[render startRender];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
#import <Flutter/Flutter.h>
|
||||||
|
|
||||||
|
@interface TGFlutterPagPlatformViewFactory : NSObject <FlutterPlatformViewFactory>
|
||||||
|
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar;
|
||||||
|
@end
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
#import "TGFlutterPagPlatformView.h"
|
||||||
|
#import "TGFlutterPagDownloadManager.h"
|
||||||
|
|
||||||
|
#import <libpag/PAGFile.h>
|
||||||
|
#import <libpag/PAGImage.h>
|
||||||
|
#import <libpag/PAGScaleMode.h>
|
||||||
|
#import <libpag/PAGView.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@interface TGFlutterPagPlatformView : NSObject <FlutterPlatformView, PAGViewListener>
|
||||||
|
@property(nonatomic, strong) PAGView* pagView;
|
||||||
|
@property(nonatomic, weak) NSObject<FlutterPluginRegistrar>* registrar;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation TGFlutterPagPlatformView
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame
|
||||||
|
viewIdentifier:(int64_t)viewId
|
||||||
|
arguments:(id)arguments
|
||||||
|
registrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_registrar = registrar;
|
||||||
|
_pagView = [[PAGView alloc] initWithFrame:frame];
|
||||||
|
_pagView.backgroundColor = UIColor.clearColor;
|
||||||
|
_pagView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||||
|
[_pagView setScaleMode:PAGScaleModeLetterBox];
|
||||||
|
[_pagView addListener:self];
|
||||||
|
[self configureWithArguments:arguments];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIView*)view {
|
||||||
|
return _pagView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)configureWithArguments:(id)arguments {
|
||||||
|
if (![arguments isKindOfClass:NSDictionary.class]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSDictionary* params = arguments;
|
||||||
|
int repeatCount = 1;
|
||||||
|
id repeatValue = params[@"repeatCount"];
|
||||||
|
if ([repeatValue respondsToSelector:@selector(intValue)]) {
|
||||||
|
repeatCount = [repeatValue intValue];
|
||||||
|
}
|
||||||
|
double initProgress = 0.0;
|
||||||
|
id progressValue = params[@"initProgress"];
|
||||||
|
if ([progressValue respondsToSelector:@selector(doubleValue)]) {
|
||||||
|
initProgress = [progressValue doubleValue];
|
||||||
|
}
|
||||||
|
BOOL autoPlay = NO;
|
||||||
|
id autoPlayValue = params[@"autoPlay"];
|
||||||
|
if ([autoPlayValue respondsToSelector:@selector(boolValue)]) {
|
||||||
|
autoPlay = [autoPlayValue boolValue];
|
||||||
|
}
|
||||||
|
NSDictionary<NSString*, NSData*>* imageReplacementsByName = [self imageReplacementsByNameFromParams:params];
|
||||||
|
|
||||||
|
[_pagView setRepeatCount:repeatCount];
|
||||||
|
[_pagView setProgress:initProgress];
|
||||||
|
|
||||||
|
NSString* assetName = [self nonEmptyString:params[@"assetName"]];
|
||||||
|
if (assetName.length > 0) {
|
||||||
|
NSString* package = [self nonEmptyString:params[@"package"]];
|
||||||
|
NSString* assetPath = [self assetPathForName:assetName package:package];
|
||||||
|
[self loadPath:assetPath initProgress:initProgress autoPlay:autoPlay imageReplacementsByName:imageReplacementsByName];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString* url = [self nonEmptyString:params[@"url"]];
|
||||||
|
if (url.length > 0) {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[TGFlutterPagDownloadManager download:url completionHandler:^(NSData* data, NSError* error) {
|
||||||
|
if (data == nil || data.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[weakSelf loadData:data initProgress:initProgress autoPlay:autoPlay imageReplacementsByName:imageReplacementsByName];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
id bytesData = params[@"bytesData"];
|
||||||
|
if ([bytesData isKindOfClass:FlutterStandardTypedData.class]) {
|
||||||
|
FlutterStandardTypedData* typedData = bytesData;
|
||||||
|
if (typedData.type == FlutterStandardDataTypeUInt8) {
|
||||||
|
[self loadData:typedData.data initProgress:initProgress autoPlay:autoPlay imageReplacementsByName:imageReplacementsByName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString*)nonEmptyString:(id)value {
|
||||||
|
if (![value isKindOfClass:NSString.class]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSString* text = [(NSString*)value stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||||
|
return text.length > 0 ? text : nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString*)assetPathForName:(NSString*)assetName package:(NSString*)package {
|
||||||
|
NSString* resourcePath = nil;
|
||||||
|
if (package.length > 0) {
|
||||||
|
resourcePath = [_registrar lookupKeyForAsset:assetName fromPackage:package];
|
||||||
|
} else {
|
||||||
|
resourcePath = [_registrar lookupKeyForAsset:assetName];
|
||||||
|
}
|
||||||
|
return [[NSBundle mainBundle] pathForResource:resourcePath ofType:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loadPath:(NSString*)path initProgress:(double)initProgress autoPlay:(BOOL)autoPlay imageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName {
|
||||||
|
if (path.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (imageReplacementsByName.count > 0) {
|
||||||
|
PAGFile* file = [PAGFile Load:path];
|
||||||
|
[self applyImageReplacementsByName:imageReplacementsByName toFile:file];
|
||||||
|
[self setFile:file initProgress:initProgress autoPlay:autoPlay];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (![_pagView setPath:path]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[_pagView setProgress:initProgress];
|
||||||
|
[_pagView flush];
|
||||||
|
if (autoPlay) {
|
||||||
|
[_pagView play];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loadData:(NSData*)data initProgress:(double)initProgress autoPlay:(BOOL)autoPlay imageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName {
|
||||||
|
if (data == nil || data.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PAGFile* file = [PAGFile Load:data.bytes size:data.length];
|
||||||
|
[self applyImageReplacementsByName:imageReplacementsByName toFile:file];
|
||||||
|
[self setFile:file initProgress:initProgress autoPlay:autoPlay];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setFile:(PAGFile*)file initProgress:(double)initProgress autoPlay:(BOOL)autoPlay {
|
||||||
|
if (file == nil || file.width <= 0 || file.height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[_pagView setComposition:file];
|
||||||
|
[_pagView setProgress:initProgress];
|
||||||
|
[_pagView flush];
|
||||||
|
if (autoPlay) {
|
||||||
|
[_pagView play];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSDictionary<NSString*, NSData*>*)imageReplacementsByNameFromParams:(NSDictionary*)params {
|
||||||
|
id raw = params[@"imageReplacementsByName"];
|
||||||
|
if (![raw isKindOfClass:NSDictionary.class]) {
|
||||||
|
return @{};
|
||||||
|
}
|
||||||
|
NSMutableDictionary<NSString*, NSData*>* result = [[NSMutableDictionary alloc] init];
|
||||||
|
[(NSDictionary*)raw enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL* stop) {
|
||||||
|
if (![key isKindOfClass:NSString.class]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString* layerName = [(NSString*)key stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||||
|
if (layerName.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSData* data = nil;
|
||||||
|
if ([value isKindOfClass:FlutterStandardTypedData.class]) {
|
||||||
|
FlutterStandardTypedData* typedData = value;
|
||||||
|
if (typedData.type == FlutterStandardDataTypeUInt8) {
|
||||||
|
data = typedData.data;
|
||||||
|
}
|
||||||
|
} else if ([value isKindOfClass:NSData.class]) {
|
||||||
|
data = value;
|
||||||
|
}
|
||||||
|
if (data.length > 0) {
|
||||||
|
result[layerName] = data;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyImageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName toFile:(PAGFile*)file {
|
||||||
|
if (file == nil || imageReplacementsByName == nil || imageReplacementsByName.count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[imageReplacementsByName enumerateKeysAndObjectsUsingBlock:^(NSString* layerName, NSData* data, BOOL* stop) {
|
||||||
|
if (![layerName isKindOfClass:NSString.class] || layerName.length == 0 ||
|
||||||
|
![data isKindOfClass:NSData.class] || data.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PAGImage* image = [PAGImage FromBytes:data.bytes size:data.length];
|
||||||
|
if (image == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[image setScaleMode:PAGScaleModeZoom];
|
||||||
|
[file replaceImageByName:layerName data:image];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
[_pagView removeListener:self];
|
||||||
|
[_pagView stop];
|
||||||
|
[_pagView setComposition:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation TGFlutterPagPlatformViewFactory {
|
||||||
|
__weak NSObject<FlutterPluginRegistrar>* _registrar;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_registrar = registrar;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSObject<FlutterMessageCodec>*)createArgsCodec {
|
||||||
|
return FlutterStandardMessageCodec.sharedInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
|
||||||
|
viewIdentifier:(int64_t)viewId
|
||||||
|
arguments:(id)args {
|
||||||
|
return [[TGFlutterPagPlatformView alloc] initWithFrame:frame
|
||||||
|
viewIdentifier:viewId
|
||||||
|
arguments:args
|
||||||
|
registrar:_registrar];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@ -30,6 +30,7 @@ typedef void(^PAGEventCallback)(NSString *);
|
|||||||
|
|
||||||
- (instancetype)initWithPagData:(NSData*)pagData
|
- (instancetype)initWithPagData:(NSData*)pagData
|
||||||
progress:(double)initProgress
|
progress:(double)initProgress
|
||||||
|
imageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName
|
||||||
frameUpdateCallback:(FrameUpdateCallback)frameUpdateCallback
|
frameUpdateCallback:(FrameUpdateCallback)frameUpdateCallback
|
||||||
eventCallback:(PAGEventCallback)eventCallback;
|
eventCallback:(PAGEventCallback)eventCallback;
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,10 @@
|
|||||||
#import <OpenGLES/ES2/glext.h>
|
#import <OpenGLES/ES2/glext.h>
|
||||||
#import <CoreVideo/CoreVideo.h>
|
#import <CoreVideo/CoreVideo.h>
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
#include <libpag/PAGFile.h>
|
||||||
|
#include <libpag/PAGImage.h>
|
||||||
#include <libpag/PAGPlayer.h>
|
#include <libpag/PAGPlayer.h>
|
||||||
|
#include <libpag/PAGScaleMode.h>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
@ -86,6 +89,7 @@ static int64_t GetCurrentTimeUS() {
|
|||||||
|
|
||||||
- (instancetype)initWithPagData:(NSData*)pagData
|
- (instancetype)initWithPagData:(NSData*)pagData
|
||||||
progress:(double)initProgress
|
progress:(double)initProgress
|
||||||
|
imageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName
|
||||||
frameUpdateCallback:(FrameUpdateCallback)frameUpdateCallback
|
frameUpdateCallback:(FrameUpdateCallback)frameUpdateCallback
|
||||||
eventCallback:(PAGEventCallback)eventCallback
|
eventCallback:(PAGEventCallback)eventCallback
|
||||||
{
|
{
|
||||||
@ -95,6 +99,10 @@ static int64_t GetCurrentTimeUS() {
|
|||||||
_initProgress = initProgress;
|
_initProgress = initProgress;
|
||||||
if(pagData){
|
if(pagData){
|
||||||
_pagFile = [PAGFile Load:pagData.bytes size:pagData.length];
|
_pagFile = [PAGFile Load:pagData.bytes size:pagData.length];
|
||||||
|
if (_pagFile == nil || _pagFile.width <= 0 || _pagFile.height <= 0) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
[self applyImageReplacementsByName:imageReplacementsByName];
|
||||||
_player = [[PAGPlayer alloc] init];
|
_player = [[PAGPlayer alloc] init];
|
||||||
[_player setComposition:_pagFile];
|
[_player setComposition:_pagFile];
|
||||||
_surface = [PAGSurface MakeFromGPU:CGSizeMake(_pagFile.width, _pagFile.height)];
|
_surface = [PAGSurface MakeFromGPU:CGSizeMake(_pagFile.width, _pagFile.height)];
|
||||||
@ -107,8 +115,30 @@ static int64_t GetCurrentTimeUS() {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)applyImageReplacementsByName:(NSDictionary<NSString*, NSData*>*)imageReplacementsByName
|
||||||
|
{
|
||||||
|
if (_pagFile == nil || imageReplacementsByName == nil || imageReplacementsByName.count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[imageReplacementsByName enumerateKeysAndObjectsUsingBlock:^(NSString* layerName, NSData* data, BOOL* stop) {
|
||||||
|
if (![layerName isKindOfClass:NSString.class] || layerName.length == 0 ||
|
||||||
|
![data isKindOfClass:NSData.class] || data.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PAGImage* image = [PAGImage FromBytes:data.bytes size:data.length];
|
||||||
|
if (image == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[image setScaleMode:PAGScaleModeZoom];
|
||||||
|
[_pagFile replaceImageByName:layerName data:image];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)startRender
|
- (void)startRender
|
||||||
{
|
{
|
||||||
|
if (_player == nil || _surface == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_displayLink) {
|
if (!_displayLink) {
|
||||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];
|
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];
|
||||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||||
@ -121,6 +151,9 @@ static int64_t GetCurrentTimeUS() {
|
|||||||
|
|
||||||
- (void)stopRender
|
- (void)stopRender
|
||||||
{
|
{
|
||||||
|
if (_player == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_displayLink) {
|
if (_displayLink) {
|
||||||
[_displayLink invalidate];
|
[_displayLink invalidate];
|
||||||
_displayLink = nil;
|
_displayLink = nil;
|
||||||
@ -136,6 +169,9 @@ static int64_t GetCurrentTimeUS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)pauseRender{
|
- (void)pauseRender{
|
||||||
|
if (_player == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_displayLink) {
|
if (_displayLink) {
|
||||||
[_displayLink invalidate];
|
[_displayLink invalidate];
|
||||||
_displayLink = nil;
|
_displayLink = nil;
|
||||||
@ -146,12 +182,18 @@ static int64_t GetCurrentTimeUS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)setProgress:(double)progress{
|
- (void)setProgress:(double)progress{
|
||||||
|
if (_player == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
[_player setProgress:progress];
|
[_player setProgress:progress];
|
||||||
[_player flush];
|
[_player flush];
|
||||||
_frameUpdateCallback();
|
_frameUpdateCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray<NSString *> *)getLayersUnderPoint:(CGPoint)point{
|
- (NSArray<NSString *> *)getLayersUnderPoint:(CGPoint)point{
|
||||||
|
if (_player == nil) {
|
||||||
|
return @[];
|
||||||
|
}
|
||||||
NSArray<PAGLayer*>* layers = [_player getLayersUnderPoint:point];
|
NSArray<PAGLayer*>* layers = [_player getLayersUnderPoint:point];
|
||||||
NSMutableArray<NSString *> *layerNames = [[NSMutableArray alloc] init];
|
NSMutableArray<NSString *> *layerNames = [[NSMutableArray alloc] init];
|
||||||
for (PAGLayer *layer in layers) {
|
for (PAGLayer *layer in layers) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class PAGView extends StatefulWidget {
|
class PAGView extends StatefulWidget {
|
||||||
@ -45,6 +46,15 @@ class PAGView extends StatefulWidget {
|
|||||||
/// 加载失败时的默认控件构造器
|
/// 加载失败时的默认控件构造器
|
||||||
final Widget Function(BuildContext context)? defaultBuilder;
|
final Widget Function(BuildContext context)? defaultBuilder;
|
||||||
|
|
||||||
|
/// Replace editable PAG image layers by layer name before playback starts.
|
||||||
|
final Map<String, Uint8List> imageReplacementsByName;
|
||||||
|
|
||||||
|
/// Uses the native iOS PAGView instead of the texture renderer.
|
||||||
|
///
|
||||||
|
/// This is intended for local PAG assets on iOS where the texture renderer can
|
||||||
|
/// report playback events while presenting transparent frames.
|
||||||
|
final bool usePlatformView;
|
||||||
|
|
||||||
static const int REPEAT_COUNT_LOOP = -1; //无限循环
|
static const int REPEAT_COUNT_LOOP = -1; //无限循环
|
||||||
static const int REPEAT_COUNT_DEFAULT = 1; //默认仅播放一次
|
static const int REPEAT_COUNT_DEFAULT = 1; //默认仅播放一次
|
||||||
|
|
||||||
@ -61,6 +71,8 @@ class PAGView extends StatefulWidget {
|
|||||||
this.onAnimationCancel,
|
this.onAnimationCancel,
|
||||||
this.onAnimationRepeat,
|
this.onAnimationRepeat,
|
||||||
this.defaultBuilder,
|
this.defaultBuilder,
|
||||||
|
this.imageReplacementsByName = const <String, Uint8List>{},
|
||||||
|
this.usePlatformView = false,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : this.bytesData = null,
|
}) : this.bytesData = null,
|
||||||
this.assetName = null,
|
this.assetName = null,
|
||||||
@ -81,6 +93,8 @@ class PAGView extends StatefulWidget {
|
|||||||
this.onAnimationCancel,
|
this.onAnimationCancel,
|
||||||
this.onAnimationRepeat,
|
this.onAnimationRepeat,
|
||||||
this.defaultBuilder,
|
this.defaultBuilder,
|
||||||
|
this.imageReplacementsByName = const <String, Uint8List>{},
|
||||||
|
this.usePlatformView = false,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : this.bytesData = null,
|
}) : this.bytesData = null,
|
||||||
this.url = null,
|
this.url = null,
|
||||||
@ -100,6 +114,8 @@ class PAGView extends StatefulWidget {
|
|||||||
this.onAnimationCancel,
|
this.onAnimationCancel,
|
||||||
this.onAnimationRepeat,
|
this.onAnimationRepeat,
|
||||||
this.defaultBuilder,
|
this.defaultBuilder,
|
||||||
|
this.imageReplacementsByName = const <String, Uint8List>{},
|
||||||
|
this.usePlatformView = false,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : this.url = null,
|
}) : this.url = null,
|
||||||
this.assetName = null,
|
this.assetName = null,
|
||||||
@ -148,6 +164,9 @@ class PAGViewState extends State<PAGView> {
|
|||||||
static const String _argumentCacheEnabled = "cacheEnabled";
|
static const String _argumentCacheEnabled = "cacheEnabled";
|
||||||
static const String _argumentCacheSize = "cacheSize";
|
static const String _argumentCacheSize = "cacheSize";
|
||||||
static const String _argumentMultiThreadEnabled = "multiThreadEnabled";
|
static const String _argumentMultiThreadEnabled = "multiThreadEnabled";
|
||||||
|
static const String _argumentImageReplacementsByName =
|
||||||
|
'imageReplacementsByName';
|
||||||
|
static const String _platformViewType = 'flutter_pag_plugin_view';
|
||||||
|
|
||||||
// 监听该函数
|
// 监听该函数
|
||||||
static const String _playCallback = 'PAGCallback';
|
static const String _playCallback = 'PAGCallback';
|
||||||
@ -161,7 +180,8 @@ class PAGViewState extends State<PAGView> {
|
|||||||
static MethodChannel _channel = (const MethodChannel('flutter_pag_plugin')
|
static MethodChannel _channel = (const MethodChannel('flutter_pag_plugin')
|
||||||
..setMethodCallHandler((result) {
|
..setMethodCallHandler((result) {
|
||||||
if (result.method == _playCallback) {
|
if (result.method == _playCallback) {
|
||||||
callbackHandlers[result.arguments[_argumentTextureId]]?.call(result.arguments[_argumentEvent]);
|
callbackHandlers[result.arguments[_argumentTextureId]]
|
||||||
|
?.call(result.arguments[_argumentEvent]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future<dynamic>.value();
|
return Future<dynamic>.value();
|
||||||
@ -172,21 +192,60 @@ class PAGViewState extends State<PAGView> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
newTexture();
|
if (!_usesPlatformView) {
|
||||||
|
newTexture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _usesPlatformView =>
|
||||||
|
widget.usePlatformView && defaultTargetPlatform == TargetPlatform.iOS;
|
||||||
|
|
||||||
|
Map<String, dynamic> get _creationParams {
|
||||||
|
final repeatCount = widget.repeatCount <= 0 &&
|
||||||
|
widget.repeatCount != PAGView.REPEAT_COUNT_LOOP
|
||||||
|
? PAGView.REPEAT_COUNT_DEFAULT
|
||||||
|
: widget.repeatCount;
|
||||||
|
final initProcess = widget.initProgress < 0 ? 0 : widget.initProgress;
|
||||||
|
return <String, dynamic>{
|
||||||
|
_argumentAssetName: widget.assetName,
|
||||||
|
_argumentPackage: widget.package,
|
||||||
|
_argumentUrl: widget.url,
|
||||||
|
_argumentBytes: widget.bytesData,
|
||||||
|
_argumentRepeatCount: repeatCount,
|
||||||
|
_argumentInitProgress: initProcess,
|
||||||
|
_argumentAutoPlay: widget.autoPlay,
|
||||||
|
_argumentImageReplacementsByName:
|
||||||
|
_validImageReplacements(widget.imageReplacementsByName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Uint8List> _validImageReplacements(
|
||||||
|
Map<String, Uint8List> replacements,
|
||||||
|
) {
|
||||||
|
if (replacements.isEmpty) {
|
||||||
|
return const <String, Uint8List>{};
|
||||||
|
}
|
||||||
|
final result = <String, Uint8List>{};
|
||||||
|
replacements.forEach((key, value) {
|
||||||
|
final layerName = key.trim();
|
||||||
|
if (layerName.isNotEmpty && value.isNotEmpty) {
|
||||||
|
result[layerName] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
void newTexture() async {
|
void newTexture() async {
|
||||||
int repeatCount = widget.repeatCount <= 0 && widget.repeatCount != PAGView.REPEAT_COUNT_LOOP ? PAGView.REPEAT_COUNT_DEFAULT : widget.repeatCount;
|
|
||||||
double initProcess = widget.initProgress < 0 ? 0 : widget.initProgress;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dynamic result =
|
dynamic result =
|
||||||
await _channel.invokeMethod(_nativeInit, {_argumentAssetName: widget.assetName, _argumentPackage: widget.package, _argumentUrl: widget.url, _argumentBytes: widget.bytesData, _argumentRepeatCount: repeatCount, _argumentInitProgress: initProcess, _argumentAutoPlay: widget.autoPlay});
|
await _channel.invokeMethod(_nativeInit, _creationParams);
|
||||||
if (result is Map) {
|
if (result is Map) {
|
||||||
_textureId = result[_argumentTextureId];
|
_textureId = result[_argumentTextureId];
|
||||||
rawWidth = result[_argumentWidth] ?? 0;
|
rawWidth = result[_argumentWidth] ?? 0;
|
||||||
rawHeight = result[_argumentHeight] ?? 0;
|
rawHeight = result[_argumentHeight] ?? 0;
|
||||||
|
} else {
|
||||||
|
throw StateError('initPag failed: $result');
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -216,6 +275,9 @@ class PAGViewState extends State<PAGView> {
|
|||||||
|
|
||||||
/// 开始
|
/// 开始
|
||||||
void start() {
|
void start() {
|
||||||
|
if (_usesPlatformView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_hasLoadTexture) {
|
if (!_hasLoadTexture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -224,6 +286,9 @@ class PAGViewState extends State<PAGView> {
|
|||||||
|
|
||||||
/// 停止
|
/// 停止
|
||||||
void stop() {
|
void stop() {
|
||||||
|
if (_usesPlatformView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_hasLoadTexture) {
|
if (!_hasLoadTexture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -232,6 +297,9 @@ class PAGViewState extends State<PAGView> {
|
|||||||
|
|
||||||
/// 暂停
|
/// 暂停
|
||||||
void pause() {
|
void pause() {
|
||||||
|
if (_usesPlatformView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_hasLoadTexture) {
|
if (!_hasLoadTexture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -240,22 +308,43 @@ class PAGViewState extends State<PAGView> {
|
|||||||
|
|
||||||
/// 设置进度
|
/// 设置进度
|
||||||
void setProgress(double progress) {
|
void setProgress(double progress) {
|
||||||
|
if (_usesPlatformView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_hasLoadTexture) {
|
if (!_hasLoadTexture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_channel.invokeMethod(_nativeSetProgress, {_argumentTextureId: _textureId, _argumentProgress: progress});
|
_channel.invokeMethod(_nativeSetProgress,
|
||||||
|
{_argumentTextureId: _textureId, _argumentProgress: progress});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取某一位置的图层
|
/// 获取某一位置的图层
|
||||||
Future<List<String>> getLayersUnderPoint(double x, double y) async {
|
Future<List<String>> getLayersUnderPoint(double x, double y) async {
|
||||||
if (!_hasLoadTexture) {
|
if (_usesPlatformView || !_hasLoadTexture) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return (await _channel.invokeMethod(_nativeGetPointLayer, {_argumentTextureId: _textureId, _argumentPointX: x, _argumentPointY: y}) as List).map((e) => e.toString()).toList();
|
return (await _channel.invokeMethod(_nativeGetPointLayer, {
|
||||||
|
_argumentTextureId: _textureId,
|
||||||
|
_argumentPointX: x,
|
||||||
|
_argumentPointY: y
|
||||||
|
}) as List)
|
||||||
|
.map((e) => e.toString())
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_usesPlatformView) {
|
||||||
|
return SizedBox(
|
||||||
|
width: widget.width ?? defaultSize,
|
||||||
|
height: widget.height ?? defaultSize,
|
||||||
|
child: UiKitView(
|
||||||
|
viewType: _platformViewType,
|
||||||
|
creationParams: _creationParams,
|
||||||
|
creationParamsCodec: const StandardMessageCodec(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (_hasLoadTexture) {
|
if (_hasLoadTexture) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: widget.width ?? (rawWidth / 2),
|
width: widget.width ?? (rawWidth / 2),
|
||||||
@ -274,8 +363,10 @@ class PAGViewState extends State<PAGView> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_channel.invokeMethod(_nativeRelease, {_argumentTextureId: _textureId});
|
if (!_usesPlatformView) {
|
||||||
callbackHandlers.remove(_textureId);
|
_channel.invokeMethod(_nativeRelease, {_argumentTextureId: _textureId});
|
||||||
|
callbackHandlers.remove(_textureId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,16 +376,19 @@ typedef PAGCallback = void Function();
|
|||||||
class PAG {
|
class PAG {
|
||||||
// 是否开启缓存,默认true
|
// 是否开启缓存,默认true
|
||||||
static void enableCache(bool enable) {
|
static void enableCache(bool enable) {
|
||||||
PAGViewState._channel.invokeMethod(PAGViewState._nativeEnableCache, {PAGViewState._argumentCacheEnabled: enable});
|
PAGViewState._channel.invokeMethod(PAGViewState._nativeEnableCache,
|
||||||
|
{PAGViewState._argumentCacheEnabled: enable});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 是否多线程加载和释放资源,默认true
|
// 是否多线程加载和释放资源,默认true
|
||||||
static void enableMultiThread(bool enable) {
|
static void enableMultiThread(bool enable) {
|
||||||
PAGViewState._channel.invokeMethod(PAGViewState._nativeEnableMultiThread, {PAGViewState._argumentMultiThreadEnabled: enable});
|
PAGViewState._channel.invokeMethod(PAGViewState._nativeEnableMultiThread,
|
||||||
|
{PAGViewState._argumentMultiThreadEnabled: enable});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置缓存数量,默认10
|
// 设置缓存数量,默认10
|
||||||
static void setCacheSize(int size) {
|
static void setCacheSize(int size) {
|
||||||
PAGViewState._channel.invokeMethod(PAGViewState._nativeSetCacheSize, {PAGViewState._argumentCacheSize: size});
|
PAGViewState._channel.invokeMethod(PAGViewState._nativeSetCacheSize,
|
||||||
|
{PAGViewState._argumentCacheSize: size});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -175,8 +175,10 @@ flutter:
|
|||||||
- sc_images/room/cp_progress/
|
- sc_images/room/cp_progress/
|
||||||
- sc_images/room/cp_invite/
|
- sc_images/room/cp_invite/
|
||||||
- sc_images/room/background_examples/
|
- sc_images/room/background_examples/
|
||||||
|
- sc_images/room/cp_broadcast/
|
||||||
- sc_images/room/emoji/
|
- sc_images/room/emoji/
|
||||||
- sc_images/room/entrance/
|
- sc_images/room/entrance/
|
||||||
|
- sc_images/room/mic/
|
||||||
- sc_images/room/red_packet/
|
- sc_images/room/red_packet/
|
||||||
- sc_images/room/rocket/
|
- sc_images/room/rocket/
|
||||||
- sc_images/room/anim/
|
- sc_images/room/anim/
|
||||||
|
|||||||
BIN
sc_images/room/anim/game_win.svga
Normal file
BIN
sc_images/room/anim/game_win.svga
Normal file
Binary file not shown.
BIN
sc_images/room/cp_broadcast/CP_broadcast.pag
Normal file
BIN
sc_images/room/cp_broadcast/CP_broadcast.pag
Normal file
Binary file not shown.
BIN
sc_images/room/cp_broadcast/brother_broadcast.pag
Normal file
BIN
sc_images/room/cp_broadcast/brother_broadcast.pag
Normal file
Binary file not shown.
BIN
sc_images/room/cp_broadcast/sisters_broadcast.pag
Normal file
BIN
sc_images/room/cp_broadcast/sisters_broadcast.pag
Normal file
Binary file not shown.
BIN
sc_images/room/mic/CP_mic_1.pag
Normal file
BIN
sc_images/room/mic/CP_mic_1.pag
Normal file
Binary file not shown.
BIN
sc_images/room/mic/CP_mic_2.svga
Normal file
BIN
sc_images/room/mic/CP_mic_2.svga
Normal file
Binary file not shown.
BIN
sc_images/room/mic/CP_mic_3.svga
Normal file
BIN
sc_images/room/mic/CP_mic_3.svga
Normal file
Binary file not shown.
BIN
sc_images/room/mic/CP_mic_4.pag
Normal file
BIN
sc_images/room/mic/CP_mic_4.pag
Normal file
Binary file not shown.
BIN
sc_images/room/mic/CP_mic_5.pag
Normal file
BIN
sc_images/room/mic/CP_mic_5.pag
Normal file
Binary file not shown.
BIN
sc_images/room/mic/brother_mic.pag
Normal file
BIN
sc_images/room/mic/brother_mic.pag
Normal file
Binary file not shown.
BIN
sc_images/room/mic/sisters_mic.pag
Normal file
BIN
sc_images/room/mic/sisters_mic.pag
Normal file
Binary file not shown.
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_system_invit_message_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/sc_system_invit_message_res.dart';
|
||||||
@ -32,6 +34,12 @@ void main() {
|
|||||||
expect(scNormalizeCpRelationType('bro'), 'BROTHER');
|
expect(scNormalizeCpRelationType('bro'), 'BROTHER');
|
||||||
expect(scNormalizeCpRelationType('sis'), 'SISTERS');
|
expect(scNormalizeCpRelationType('sis'), 'SISTERS');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('relation days display starts from day one', () {
|
||||||
|
expect(scCpRelationDisplayDays('0'), '1');
|
||||||
|
expect(scCpRelationDisplayDays('12'), '13');
|
||||||
|
expect(scCpRelationDisplayDays(''), '1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('scCpRelationTypeForGift', () {
|
group('scCpRelationTypeForGift', () {
|
||||||
@ -68,4 +76,26 @@ void main() {
|
|||||||
expect(invite.toJson()['userHeaddress'], 'frame.svga');
|
expect(invite.toJson()['userHeaddress'], 'frame.svga');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('SCSystemInvitMessageRes CP_BUILD relation type', () {
|
||||||
|
test('uses top-level relation type when content omits it', () {
|
||||||
|
final invite = SCSystemInvitMessageRes.fromMessageData({
|
||||||
|
'relationType': 'BROTHER',
|
||||||
|
'content': jsonEncode({'userNickname': 'Alex'}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(invite?.relationType, 'BROTHER');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('infers relation type from gift payload when needed', () {
|
||||||
|
final invite = SCSystemInvitMessageRes.fromMessageData({
|
||||||
|
'content': {
|
||||||
|
'userNickname': 'Sam',
|
||||||
|
'gift': {'giftCode': 'close_friend_sister_ring'},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(invite?.relationType, 'SISTERS');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dar
|
|||||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
|
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('maps localized rocket rule html from go rule response', () {
|
test('maps localized rocket rule html from go rule response', () {
|
||||||
@ -41,6 +42,42 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('formats rocket rule html lists without detached markers', () {
|
||||||
|
final text = debugRoomRocketRuleHtmlPlainText(
|
||||||
|
'<h2>How to Launch a Rocket</h2>'
|
||||||
|
'<ol>'
|
||||||
|
'<li><p>Sending gifts in the room will add fuel.</p></li>'
|
||||||
|
'<li><p>Once the rocket fuel is full, it will launch.</p></li>'
|
||||||
|
'</ol>',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(text, contains('How to Launch a Rocket\n1. Sending gifts'));
|
||||||
|
expect(text, contains('\n2. Once the rocket fuel is full'));
|
||||||
|
expect(text, isNot(contains('1. \n')));
|
||||||
|
expect(text, isNot(contains('2. \n')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps manually numbered rocket rule html from duplicating markers', () {
|
||||||
|
final text = debugRoomRocketRuleHtmlPlainText(
|
||||||
|
'<h2>How to Launch a Rocket</h2>'
|
||||||
|
'<ol>'
|
||||||
|
'<li><p>1. Sending gifts in the room will add fuel.</p></li>'
|
||||||
|
'<li><p>2. Once the rocket fuel is full, it will launch.</p></li>'
|
||||||
|
'<li><p>3. The higher the rocket level, the more rewards.</p></li>'
|
||||||
|
'<li><p><strong>Leaderboard and Rewards</strong></p></li>'
|
||||||
|
'</ol>'
|
||||||
|
'<ol>'
|
||||||
|
'<li><p>1. The Top 1 user will receive an exclusive reward.</p></li>'
|
||||||
|
'</ol>',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(text, contains('How to Launch a Rocket\n1. Sending gifts'));
|
||||||
|
expect(text, contains('\nLeaderboard and Rewards\n1. The Top 1 user'));
|
||||||
|
expect(text, isNot(contains('1. \n1.')));
|
||||||
|
expect(text, isNot(contains('4. \nLeaderboard')));
|
||||||
|
expect(text, isNot(contains('4. Leaderboard')));
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'parses level-specific reward previews and rocket kings from levels',
|
'parses level-specific reward previews and rocket kings from levels',
|
||||||
() {
|
() {
|
||||||
|
|||||||
65
test/sc_cp_relation_pair_utils_test.dart
Normal file
65
test/sc_cp_relation_pair_utils_test.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_pair_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('scCpRelationTypeBetweenProfiles', () {
|
||||||
|
test('detects cp relation from either adjacent profile', () {
|
||||||
|
final left = SocialChatUserProfile(
|
||||||
|
id: '100',
|
||||||
|
account: 'left',
|
||||||
|
cpList: [
|
||||||
|
CPRes(
|
||||||
|
meUserId: '100',
|
||||||
|
meAccount: 'left',
|
||||||
|
cpUserId: '200',
|
||||||
|
cpAccount: 'right',
|
||||||
|
relationType: 'CP',
|
||||||
|
levelText: 'Lv.3 Sweet',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final right = SocialChatUserProfile(id: '200', account: 'right');
|
||||||
|
|
||||||
|
expect(scCpRelationTypeBetweenProfiles(left, right), 'CP');
|
||||||
|
expect(scCpRelationTypeBetweenProfiles(right, left), 'CP');
|
||||||
|
expect(scCpRelationInfoBetweenProfiles(left, right)?.cpLevel, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('detects brother and sister close-friend relations', () {
|
||||||
|
final brotherLeft = SocialChatUserProfile(
|
||||||
|
id: '100',
|
||||||
|
closeFriendList: [
|
||||||
|
CPRes(meUserId: '100', cpUserId: '200', relationType: 'bro'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final brotherRight = SocialChatUserProfile(id: '200');
|
||||||
|
final sisterLeft = SocialChatUserProfile(id: '300');
|
||||||
|
final sisterRight = SocialChatUserProfile(
|
||||||
|
id: '400',
|
||||||
|
closeFriendList: [
|
||||||
|
CPRes(meUserId: '300', cpUserId: '400', relationType: 'sis'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
scCpRelationTypeBetweenProfiles(brotherLeft, brotherRight),
|
||||||
|
'BROTHER',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
scCpRelationTypeBetweenProfiles(sisterLeft, sisterRight),
|
||||||
|
'SISTERS',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not match unrelated profiles', () {
|
||||||
|
final left = SocialChatUserProfile(
|
||||||
|
id: '100',
|
||||||
|
cpList: [CPRes(meUserId: '100', cpUserId: '200', relationType: 'CP')],
|
||||||
|
);
|
||||||
|
final right = SocialChatUserProfile(id: '300');
|
||||||
|
|
||||||
|
expect(scCpRelationTypeBetweenProfiles(left, right), isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
64
需求进度.md
64
需求进度.md
@ -1,5 +1,6 @@
|
|||||||
# 首充弹窗需求进度(2026-05-21)
|
# 首充弹窗需求进度(2026-05-21)
|
||||||
|
|
||||||
|
- [ ] 2026-05-22 新增问题:后台首充已配置,但前端未显示首充弹窗,房间礼物面板也未显示首充悬浮板;本轮需用 iPhone 17 Pro 模拟器实际排查并修复。
|
||||||
- [x] 2026-05-22 用户反馈:礼物弹窗在 iPhone 17 Pro Max 仍报 `A Stack requires bounded constraints from its parent`;本轮已实际运行 iPhone 17 Pro Max 复现,并依据运行日志修到不再报错。
|
- [x] 2026-05-22 用户反馈:礼物弹窗在 iPhone 17 Pro Max 仍报 `A Stack requires bounded constraints from its parent`;本轮已实际运行 iPhone 17 Pro Max 复现,并依据运行日志修到不再报错。
|
||||||
- [x] 2026-05-22 已确认 iPhone 17 Pro Max 模拟器在线,设备 ID:`8D2D87EB-4769-43B7-8DC2-580D46A76B6C`。
|
- [x] 2026-05-22 已确认 iPhone 17 Pro Max 模拟器在线,设备 ID:`8D2D87EB-4769-43B7-8DC2-580D46A76B6C`。
|
||||||
- [x] 2026-05-22 已在 iPhone 17 Pro Max 启动主 App;控制台先出现 SmartDialog overlay 运行期异常:`RenderAnimatedOpacity ... SmartDialogWidget ... size: MISSING`,说明问题发生在弹窗 overlay 内容尺寸约束层。
|
- [x] 2026-05-22 已在 iPhone 17 Pro Max 启动主 App;控制台先出现 SmartDialog overlay 运行期异常:`RenderAnimatedOpacity ... SmartDialogWidget ... size: MISSING`,说明问题发生在弹窗 overlay 内容尺寸约束层。
|
||||||
@ -916,3 +917,66 @@
|
|||||||
- 如果需要正式 iOS 分发包,为 `scripts/build_release.py` 补入 `--ios-codesign` 所需签名参数并导出 `.ipa`。
|
- 如果需要正式 iOS 分发包,为 `scripts/build_release.py` 补入 `--ios-codesign` 所需签名参数并导出 `.ipa`。
|
||||||
- 继续评估 Agora 扩展库是否都需要,优先核查 `lip sync`、`spatial audio`、`clear vision`、`segmentation`、`face capture` 等扩展 so 能否裁剪。
|
- 继续评估 Agora 扩展库是否都需要,优先核查 `lip sync`、`spatial audio`、`clear vision`、`segmentation`、`face capture` 等扩展 so 能否裁剪。
|
||||||
- 决定是否处理剩余 12 张特殊 `webp`:改格式、换压缩工具,或维持现状。
|
- 决定是否处理剩余 12 张特殊 `webp`:改格式、换压缩工具,或维持现状。
|
||||||
|
- [ ] 2026-05-22 16:08:31 已确认 iPhone 17 Pro 模拟器在线,并开始检查首充弹窗/礼物悬浮板显示条件。
|
||||||
|
- [ ] 2026-05-22 16:08:53 已读取首充接口模型、弹窗加载逻辑、礼物面板悬浮板条件和首页首充入口,准备定位显示断点。
|
||||||
|
- [ ] 2026-05-22 16:09:12 已确认礼物面板悬浮板仍依赖登录资料 firstRecharge,本地还需接入首充奖励 home 接口状态。
|
||||||
|
- [ ] 2026-05-22 16:12:00 已确认首页入场弹层队列目前只串注册奖励、运营弹窗、每日签到,没有首充弹窗主动展示步骤。
|
||||||
|
- [ ] 2026-05-22 16:12:44 准备用 iPhone 17 Pro 启动当前工程现状,复现首充弹窗和礼物面板悬浮板不显示问题。
|
||||||
|
- [ ] 2026-05-22 16:15:24 iPhone 17 Pro 当前正在编译启动 lib/main.dart,等待进入应用后对比现状。
|
||||||
|
- [ ] 2026-05-22 16:16:56 已截取 iPhone 17 Pro 修复前首页截图,用于确认当前没有首充弹窗。
|
||||||
|
- [ ] 2026-05-22 16:17:15 iPhone 17 Pro 修复前截图显示每日签到弹窗正常展示,但没有首充弹窗,运行日志也未出现首充 home 接口请求。
|
||||||
|
- [ ] 2026-05-22 16:17:34 开始读取首充奖励 Flutter 接口文档,核对 configured/enabled/activityStatus/rewarded 等字段含义。
|
||||||
|
- [ ] 2026-05-22 16:18:13 准备修改首页入场弹层队列和礼物面板首充悬浮板判断,统一由 first-recharge-reward/home 接口驱动。
|
||||||
|
- [ ] 2026-05-22 16:19:01 已完成首充入口代码修改:首页入场队列新增首充远程弹窗,礼物面板悬浮板改为读取首充 home 接口状态。
|
||||||
|
- [ ] 2026-05-22 16:19:18 开始格式化本轮修改的 gift_page.dart 和 index_page.dart。
|
||||||
|
- [ ] 2026-05-22 16:19:28 开始对首充相关修改文件执行 flutter analyze。
|
||||||
|
- [ ] 2026-05-22 16:20:32 flutter analyze 发现 index_page.dart 缺少 AccountStorage 导入,准备补齐后复查。
|
||||||
|
- [ ] 2026-05-22 16:21:15 已补齐 AccountStorage 导入,重新格式化并执行 flutter analyze。
|
||||||
|
- [ ] 2026-05-22 16:22:10 flutter analyze 通过,首充入口修改当前无静态错误。
|
||||||
|
- [ ] 2026-05-22 16:22:37 准备对 iPhone 17 Pro 当前 flutter run 会话执行 hot restart 验证首页首充弹窗。
|
||||||
|
- [ ] 2026-05-22 16:23:24 已截取 iPhone 17 Pro 首页热重启后截图,检查首充弹窗是否展示。
|
||||||
|
- [ ] 2026-05-22 16:24:03 iPhone 17 Pro 热重启后已看到首充弹窗,首页入场首充接口展示链路验证通过;继续验证房间礼物面板悬浮板。
|
||||||
|
- [ ] 2026-05-22 16:26:26 已在 iPhone 17 Pro 进入房间并打开礼物面板,非 CP 礼物 tab 上方已显示首充悬浮板并完成截图。
|
||||||
|
- [ ] 2026-05-22 16:27:08 已切到 CP tab,确认顶部为 CP 专属悬浮板,不显示首充悬浮板,并保存截图。
|
||||||
|
- [ ] 2026-05-22 16:30:42 已点击礼物面板首充悬浮板,确认会关闭礼物面板并打开首充弹窗,已保存截图。
|
||||||
|
- [ ] 2026-05-22 16:31:26 验证礼物悬浮板点击可打开首充弹窗;同时发现进房时有既有 Provider build 期 notify 异常,开始确认是否需一并修复。
|
||||||
|
- [ ] 2026-05-22 16:33:39 准备修复 voice_room_page 在 build 阶段同步通知 RtcProvider 导致的 setState/markNeedsBuild 异常。
|
||||||
|
- [ ] 2026-05-22 16:34:37 已将房间页 Provider 通知延后到首帧后,开始格式化并重新执行 flutter analyze。
|
||||||
|
- [ ] 2026-05-22 16:36:03 flutter analyze 再次通过,包含首充入口和房间页 Provider 通知修复。
|
||||||
|
- [ ] 2026-05-22 16:36:23 准备再次 hot restart iPhone 17 Pro,用干净日志复验首页弹窗、进房、礼物悬浮板和 CP 悬浮板。
|
||||||
|
- [ ] 2026-05-22 16:38:24 最终复验:iPhone 17 Pro hot restart 后首页首充弹窗再次正常显示并截图。
|
||||||
|
- [ ] 2026-05-22 16:40:39 新增反馈:首充奖励 item 不能固定占六个,空数据不显示;启动需补接口决定的进场弹窗;弹窗顺序调整为注册奖励 > 每日签到 > 进场弹窗 > 首充弹窗。
|
||||||
|
- [ ] 2026-05-22 16:41:07 开始读取 APP 进场弹窗接口文档、首页启动弹窗队列、进场弹窗 coordinator 和响应模型。
|
||||||
|
- [ ] 2026-05-22 16:41:43 已定位进场弹窗路径和启动顺序问题,继续检查首充弹窗 rewards grid 是否固定占位。
|
||||||
|
- [ ] 2026-05-22 16:44:01 准备修改进场接口路径、首页启动弹窗顺序,以及首充奖励 item 的空数据过滤/动态数量渲染。
|
||||||
|
- [ ] 2026-05-22 16:46:18 已修改首充 rewards 过滤/动态渲染、首页弹窗顺序和进场弹窗接口路径。
|
||||||
|
- [ ] 2026-05-22 16:46:39 开始格式化并分析首充弹窗、首页启动队列、进场接口仓库和相关页面。
|
||||||
|
- [ ] 2026-05-22 16:48:20 flutter analyze 通过,首充动态 item、进场接口路径和弹窗顺序修改无静态错误。
|
||||||
|
- [ ] 2026-05-22 16:48:51 准备为进场弹窗 coordinator 开启 debug 日志,便于模拟器验证接口队列和本地 limit 判断。
|
||||||
|
- [ ] 2026-05-22 16:49:39 已开启 EntryPopup debug 日志,开始格式化并重新分析相关文件。
|
||||||
|
- [ ] 2026-05-22 16:50:21 flutter analyze 发现 entry popup coordinator 存在空 catch info,准备改为 debug 日志。
|
||||||
|
- [ ] 2026-05-22 16:50:51 已补齐 entry popup 异常 debug 日志,重新执行格式化和分析。
|
||||||
|
- [ ] 2026-05-22 16:51:45 flutter analyze 最终通过,进场弹窗 debug 日志改动无静态问题。
|
||||||
|
- [ ] 2026-05-22 16:52:50 已复查代码:启动顺序为注册奖励 > 每日签到 > 进场弹窗 > 首充弹窗,首充奖励只生成有效 item,进场接口路径已对齐文档。
|
||||||
|
- [ ] 2026-05-22 16:53:25 准备用 iPhone 17 Pro 重新启动验证:检查弹窗顺序、进场弹窗接口和首充动态 item 显示。
|
||||||
|
- [ ] 2026-05-22 16:54:59 iPhone 17 Pro 正在编译启动 lib/main.dart,等待进入首页验证弹窗顺序。
|
||||||
|
- [ ] 2026-05-22 16:55:57 已截取 iPhone 17 Pro 启动后第一阶段弹窗截图。
|
||||||
|
- [ ] 2026-05-22 16:56:28 iPhone 17 Pro 启动第一阶段确认展示每日签到弹窗,符合注册奖励之后、进场弹窗之前的顺序要求。
|
||||||
|
- [ ] 2026-05-22 17:01:20 iPhone 17 Pro 日志显示 /app/popups/entry 当前环境返回 404,准备增加 /go/app/popups/entry 兼容降级。
|
||||||
|
- [ ] 2026-05-22 17:03:08 已为进场弹窗接口增加 404 降级到 /go/app/popups/entry,开始格式化并分析。
|
||||||
|
- [ ] 2026-05-22 17:04:34 flutter analyze 通过,进场弹窗接口降级兼容无静态问题。
|
||||||
|
- [ ] 2026-05-22 17:05:08 准备 hot restart iPhone 17 Pro,复验进场接口兼容降级和弹窗队列。
|
||||||
|
- [ ] 2026-05-22 17:07:03 hot restart 后已重新截取启动队列第一阶段截图。
|
||||||
|
- [ ] 2026-05-22 17:13:01 EntryPopup 日志确认接口已返回 queue=[invite, game],但当前 invite 图片为 cdn.example.com 示例地址,预加载失败所以按规则跳过展示;准备修正金币 item 过滤。
|
||||||
|
- [ ] 2026-05-22 17:14:16 已放宽金币奖励有效判断,金币金额在 content/displayTitle 时也保留;开始格式化和分析。
|
||||||
|
- [ ] 2026-05-22 17:15:21 flutter analyze 通过,金币奖励 content/displayTitle 保留逻辑无静态错误。
|
||||||
|
- [ ] 2026-05-22 17:15:52 准备再次 hot restart 验证首充弹窗:金币奖励应保留,空 item 不显示。
|
||||||
|
- [ ] 2026-05-22 17:18:02 已等待启动队列推进并截取首充弹窗动态 item 截图。
|
||||||
|
- [ ] 2026-05-22 17:19:23 hot restart 后 9 秒仍停留在每日签到,确认启动队列会等待前一个弹窗结束再继续。
|
||||||
|
- [ ] 2026-05-22 17:20:46 已截取最终首充弹窗截图:金币奖励保留,两个空占位 item 已不显示。
|
||||||
|
- [ ] 2026-05-22 17:22:05 准备优化进场弹窗候选选择:队列中某个弹窗图片预加载失败时,继续尝试下一个可展示候选。
|
||||||
|
- [ ] 2026-05-22 17:23:10 已让进场弹窗图片失败时继续尝试下一个候选,开始格式化并分析。
|
||||||
|
- [ ] 2026-05-22 17:24:18 flutter analyze 通过,进场弹窗候选兜底逻辑无静态问题。
|
||||||
|
- [ ] 2026-05-22 17:26:58 准备热加载进场弹窗候选兜底逻辑,并通过前后台切换触发一次 entry popup 验证日志。
|
||||||
|
- [ ] 2026-05-22 17:31:21 当前 iPhone 17 Pro flutter run 会话已停止,避免后台继续占用;候选兜底逻辑已通过 flutter analyze,未再做前后台日志复验。
|
||||||
|
- [x] 2026-05-22 17:32:46 本轮完成:首充奖励按有效接口 item 动态渲染;启动弹窗顺序调整为注册奖励 > 每日签到 > 进场弹窗 > 首充弹窗;进场弹窗接口已接入并支持路径降级与候选图片兜底。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user