diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3904519..536c901 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } android { - namespace = "com.org.yumi" + namespace = "com.org.yumiparty" compileSdk = 36 ndkVersion = "28.2.13676358" @@ -58,7 +58,7 @@ android { manifestPlaceholders["appName"] = "Yumi" manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher" manifestPlaceholders["appIconRound"] = "@mipmap/icon_logo_round" - applicationId = "com.org.yumi" + applicationId = "com.org.yumiparty" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/google-services.json b/android/app/google-services.json index 3054e12..ce18278 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -9,7 +9,7 @@ "client_info": { "mobilesdk_app_id": "1:980005024266:android:581aa38059aa318d9c65f3", "android_client_info": { - "package_name": "com.org.yumi" + "package_name": "com.org.yumiparty" } }, "oauth_client": [ @@ -17,7 +17,7 @@ "client_id": "980005024266-o9pjdmdbqqt1julbh1q1ovafcvmr1mv1.apps.googleusercontent.com", "client_type": 1, "android_info": { - "package_name": "com.org.yumi", + "package_name": "com.org.yumiparty", "certificate_hash": "3fe178dcc3294f7420df2754cfaf4646d6a19478" } }, @@ -25,7 +25,7 @@ "client_id": "980005024266-sl5h466pe90jsjmoi4jcd7bqhmckieec.apps.googleusercontent.com", "client_type": 1, "android_info": { - "package_name": "com.org.yumi", + "package_name": "com.org.yumiparty", "certificate_hash": "9ab21924bfbe2c56555860ebabf23c4099fd7412" } }, @@ -33,7 +33,7 @@ "client_id": "980005024266-vrrp2us89svbqgc93oe7q4ad7uoh8fl6.apps.googleusercontent.com", "client_type": 1, "android_info": { - "package_name": "com.org.yumi", + "package_name": "com.org.yumiparty", "certificate_hash": "c180afbf6e1ff2f449587287a2bc5407e7d2d9a3" } }, diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index fbecac5..5b6c7c4 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -28,7 +28,7 @@ -keep class io.flutter.embedding.engine.deferredcomponents.** { *; } # 应用特定 --keep class com.org.yumi.** { *; } +-keep class com.org.yumiparty.** { *; } # Provider -keep class * extends ChangeNotifier { *; } @@ -36,4 +36,4 @@ # 其他保留 -keepattributes *Annotation* --keepattributes Signature \ No newline at end of file +-keepattributes Signature diff --git a/android/app/src/main/kotlin/com/tkm/likei/variant1/MainActivity.kt b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt similarity index 60% rename from android/app/src/main/kotlin/com/tkm/likei/variant1/MainActivity.kt rename to android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt index e3fe890..d8110ee 100644 --- a/android/app/src/main/kotlin/com/tkm/likei/variant1/MainActivity.kt +++ b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt @@ -1,13 +1,10 @@ -package com.org.yumi - -import android.content.Intent -import android.net.Uri -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel - -class MainActivity: FlutterActivity() { - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) // 这行必须存在! - } -} +package com.org.yumiparty + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +class MainActivity : FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) // 这行必须存在! + } +} diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 6ed344c..dd7ff4c 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -2,4 +2,4 @@ #include "Generated.xcconfig" APP_DISPLAY_NAME=yumi -PRODUCT_BUNDLE_IDENTIFIER=com.org.yumi +PRODUCT_BUNDLE_IDENTIFIER=com.org.yumiparty diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index e5efcef..805b226 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -2,4 +2,4 @@ #include "Generated.xcconfig" APP_DISPLAY_NAME=yumi -PRODUCT_BUNDLE_IDENTIFIER=com.org.yumi +PRODUCT_BUNDLE_IDENTIFIER=com.org.yumiparty diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 76f33f6..420d4b1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -512,7 +512,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.org.yumi; + PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -703,7 +703,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.org.yumi; + PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -732,7 +732,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.org.yumi; + PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist index aa092d5..032ee51 100644 --- a/ios/Runner/GoogleService-Info.plist +++ b/ios/Runner/GoogleService-Info.plist @@ -15,7 +15,7 @@ PLIST_VERSION 1 BUNDLE_ID - com.org.yumi + com.org.yumiparty PROJECT_ID yumi-7b503 STORAGE_BUCKET @@ -33,4 +33,4 @@ GOOGLE_APP_ID 1:52796773508:ios:7dfa56d19e1711ab65ded1 - \ No newline at end of file + diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart index 8b1c3bb..288ffcf 100644 --- a/lib/app/config/configs/sc_variant1_config.dart +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -11,7 +11,7 @@ class SCVariant1Config implements AppConfig { String get appName => 'yumi'; // 马甲包应用名称 @override - String get packageName => 'com.org.yumi'; + String get packageName => 'com.org.yumiparty'; @override String get apiHost => const String.fromEnvironment( diff --git a/lib/modules/home/popular/party/sc_home_party_page.dart b/lib/modules/home/popular/party/sc_home_party_page.dart index 27ac498..b04fbb9 100644 --- a/lib/modules/home/popular/party/sc_home_party_page.dart +++ b/lib/modules/home/popular/party/sc_home_party_page.dart @@ -7,6 +7,7 @@ import 'package:pull_to_refresh/pull_to_refresh.dart'; import '../../../../app_localizations.dart'; import '../../../../app/constants/sc_global_config.dart'; import '../../../../app/routes/sc_fluro_navigator.dart'; +import '../../../../shared/business_logic/models/res/sc_index_banner_res.dart'; import '../../../../shared/tools/sc_banner_utils.dart'; import '../../../../shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import '../../../../shared/business_logic/models/res/follow_room_res.dart'; @@ -37,6 +38,7 @@ class _HomePartyPageState extends State List historyRooms = []; String? lastId; bool isLoading = false; + bool _isBannerLoading = false; bool _isLeaderboardLoading = false; final RefreshController _refreshController = RefreshController( initialRefresh: false, @@ -82,15 +84,24 @@ class _HomePartyPageState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Stack( - alignment: AlignmentDirectional.center, - children: [ - Consumer( - builder: (context, ref, child) { - return _banner(ref); - }, - ), - ], + Consumer( + builder: (context, ref, child) { + if (!_shouldShowBannerSection(ref)) { + return const SizedBox.shrink(); + } + return Padding( + padding: EdgeInsets.only(bottom: 12.w), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + if (_shouldShowBannerSkeleton(ref)) + _buildBannerSkeleton() + else + _banner(ref), + ], + ), + ); + }, ), Consumer( builder: (context, ref, child) { @@ -111,7 +122,9 @@ class _HomePartyPageState extends State } _banner(SCAppGeneralManager ref) { - if (ref.exploreBanners.isEmpty) { + final banners = _partyBanners(ref); + final hasMultipleBanners = banners.length > 1; + if (banners.isEmpty) { return Container(); } return Stack( @@ -120,15 +133,19 @@ class _HomePartyPageState extends State CarouselSlider( options: CarouselOptions( height: 95.w, - autoPlay: true, + autoPlay: hasMultipleBanners, // 启用自动播放 enlargeCenterPage: false, // 居中放大当前页面 - enableInfiniteScroll: true, + enableInfiniteScroll: hasMultipleBanners, // 启用无限循环 autoPlayAnimationDuration: Duration(milliseconds: 800), // 自动播放动画时长 viewportFraction: 1, + scrollPhysics: + hasMultipleBanners + ? const BouncingScrollPhysics() + : const NeverScrollableScrollPhysics(), // 视口分数 onPageChanged: (index, reason) { _currentIndex = index; @@ -136,7 +153,7 @@ class _HomePartyPageState extends State }, ), items: - ref.exploreBanners.map((item) { + banners.map((item) { return GestureDetector( child: netImage( url: item.cover ?? "", @@ -154,7 +171,7 @@ class _HomePartyPageState extends State Row( mainAxisAlignment: MainAxisAlignment.center, children: - ref.exploreBanners.asMap().entries.map((entry) { + banners.asMap().entries.map((entry) { return Container( width: 6.0, height: 6.0, @@ -171,6 +188,13 @@ class _HomePartyPageState extends State ); } + List _partyBanners(SCAppGeneralManager ref) { + if (ref.exploreBanners.isNotEmpty) { + return ref.exploreBanners; + } + return ref.homeBanners; + } + _banner2(SCAppGeneralManager ref) { String roomGiftOneAvatar = ""; String roomGiftTwoAvatar = ""; @@ -410,6 +434,14 @@ class _HomePartyPageState extends State return _isLeaderboardLoading && !_hasLeaderboardData(ref); } + bool _shouldShowBannerSection(SCAppGeneralManager ref) { + return _shouldShowBannerSkeleton(ref) || _partyBanners(ref).isNotEmpty; + } + + bool _shouldShowBannerSkeleton(SCAppGeneralManager ref) { + return _isBannerLoading && _partyBanners(ref).isEmpty; + } + bool _hasLeaderboardData(SCAppGeneralManager ref) { final appLeaderResult = ref.appLeaderResult; return (appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? @@ -419,6 +451,68 @@ class _HomePartyPageState extends State false); } + Widget _buildBannerSkeleton() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w), + child: SizedBox( + height: 95.w, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + border: Border.all(color: _kPartySkeletonBorder, width: 1.w), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.14), + blurRadius: 18.w, + offset: Offset(0, 8.w), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12.w), + child: Stack( + children: [ + Positioned.fill( + child: Opacity( + opacity: 0.45, + child: _buildSkeletonPanel(), + ), + ), + ], + ), + ), + ), + ), + Positioned( + bottom: 10.w, + child: Row( + children: List.generate(3, (index) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: _buildSkeletonBone( + width: 6.w, + height: 6.w, + shape: BoxShape.circle, + ), + ); + }), + ), + ), + ], + ), + ), + ); + } + Widget _buildLeaderboardSkeleton() { return SizedBox( height: 130.w, @@ -672,6 +766,9 @@ class _HomePartyPageState extends State ); setState(() { isLoading = true; + _isBannerLoading = + generalManager.exploreBanners.isEmpty && + generalManager.homeBanners.isEmpty; _isLeaderboardLoading = generalManager.appLeaderResult == null; }); SCChatRoomRepository() @@ -689,7 +786,14 @@ class _HomePartyPageState extends State isLoading = false; if (mounted) setState(() {}); }); - generalManager.loadMainBanner(); + generalManager.loadMainBanner().whenComplete(() { + if (!mounted) { + return; + } + setState(() { + _isBannerLoading = false; + }); + }); generalManager.appLeaderboard().whenComplete(() { if (!mounted) { return; diff --git a/lib/modules/room/edit/room_edit_page.dart b/lib/modules/room/edit/room_edit_page.dart index 3a7cfa0..fa1e160 100644 --- a/lib/modules/room/edit/room_edit_page.dart +++ b/lib/modules/room/edit/room_edit_page.dart @@ -174,8 +174,10 @@ class _RoomEditPageState extends State { bool success, String url, ) { + debugPrint( + "[Room Cover] upload callback success=$success url=$url", + ); if (success) { - debugPrint("[Room Cover] uploaded url: $url"); setState(() { roomCover = url; isEdit = true; @@ -523,7 +525,9 @@ class _RoomEditPageState extends State { return; } SCLoadingManager.show(context: context); - debugPrint("[Room Cover] submit roomCover: $submittedRoomCover"); + debugPrint( + "[Room Cover] submit roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""} roomCover=$submittedRoomCover roomName=$submittedRoomName roomDesc=$submittedRoomDesc", + ); var roomInfo = await SCAccountRepository().editRoomInfo( rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", submittedRoomCover, @@ -548,6 +552,9 @@ class _RoomEditPageState extends State { ); roomManager.updateMyRoomInfo(mergedRoomInfo); if (widget.needRestCurrentRoomInfo != "true") { + debugPrint( + "[Room Cover Sync] dispatch roomSettingUpdate groupId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""} roomId=${mergedRoomInfo.id ?? rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""}", + ); currentRtmProvider.dispatchMessage( Msg( groupId: diff --git a/lib/modules/user/crop/crop_image_page.dart b/lib/modules/user/crop/crop_image_page.dart index 6175931..dcac958 100644 --- a/lib/modules/user/crop/crop_image_page.dart +++ b/lib/modules/user/crop/crop_image_page.dart @@ -22,7 +22,8 @@ class CropImagePage extends StatefulWidget { final File image; // 原始图片文件 final bool needUpload; - const CropImagePage(this.image, { + const CropImagePage( + this.image, { Key? key, this.onUpLoadCallBack, this.aspectRatio, @@ -58,7 +59,7 @@ class _CropImageRouteState extends State { child: Container( width: double.infinity, height: - SCScreen.designHeight - + SCScreen.designHeight - (kToolbarHeight * 2) - ScreenUtil().bottomBarHeight - ScreenUtil().statusBarHeight, @@ -82,7 +83,8 @@ class _CropImageRouteState extends State { alignment: Alignment.centerLeft, children: [ GestureDetector( - onTap: () => SCNavigatorUtils.goBackWithParams(context, ''), + onTap: + () => SCNavigatorUtils.goBackWithParams(context, ''), child: Padding( padding: const EdgeInsets.all(8.0), child: Icon( @@ -114,8 +116,12 @@ class _CropImageRouteState extends State { color: const Color(0xff666666), alignment: Alignment.centerRight, child: GestureDetector( - onTap: () => - _cropAndUpload(context, widget.image, needUpload:widget.needUpload), + onTap: + () => _cropAndUpload( + context, + widget.image, + needUpload: widget.needUpload, + ), child: Padding( padding: const EdgeInsets.all(8.0).copyWith(right: 18), child: Text( @@ -133,17 +139,23 @@ class _CropImageRouteState extends State { } /// 使用 image_cropper 进行裁剪并上传 - Future _cropAndUpload(BuildContext context, - File originalFile, { - bool needUpload = true, - }) async { + Future _cropAndUpload( + BuildContext context, + File originalFile, { + bool needUpload = true, + }) async { + final originalExists = originalFile.existsSync(); + final originalSize = originalExists ? originalFile.lengthSync() : -1; + debugPrint( + "[Room Cover Upload] crop start originalPath=${originalFile.path} originalExists=$originalExists originalSize=$originalSize needUpload=$needUpload aspectRatio=${widget.aspectRatio}", + ); // 调用系统裁剪界面 CroppedFile? cropped = await ImageCropper().cropImage( sourcePath: originalFile.path, aspectRatio: - widget.aspectRatio != null - ? CropAspectRatio(ratioX: widget.aspectRatio!, ratioY: 1) - : null, + widget.aspectRatio != null + ? CropAspectRatio(ratioX: widget.aspectRatio!, ratioY: 1) + : null, uiSettings: [ AndroidUiSettings( toolbarTitle: SCAppLocalizations.of(context)!.crop, @@ -168,12 +180,21 @@ class _CropImageRouteState extends State { if (cropped == null) { if (widget.backOriginalFile ?? true) { fileToUpload = originalFile; + debugPrint( + "[Room Cover Upload] crop cancelled, fallback to original file path=${fileToUpload.path}", + ); } else { + debugPrint("[Room Cover Upload] crop cancelled, no fallback file"); widget.onUpLoadCallBack?.call(false, ""); return; } } else { fileToUpload = File(cropped.path); + final croppedExists = fileToUpload.existsSync(); + final croppedSize = croppedExists ? fileToUpload.lengthSync() : -1; + debugPrint( + "[Room Cover Upload] crop result path=${fileToUpload.path} exists=$croppedExists size=$croppedSize", + ); } if (needUpload) { // 弹出上传中的 loading @@ -188,12 +209,22 @@ class _CropImageRouteState extends State { /// 上传文件至oss Future upload(File file) async { try { + final fileExists = file.existsSync(); + final fileSize = fileExists ? file.lengthSync() : -1; + debugPrint( + "[Room Cover Upload] local file before request path=${file.path} exists=$fileExists size=$fileSize", + ); String fileUrl = await SCGeneralRepositoryImp().upload(file); + debugPrint("[Room Cover Upload] upload success fileUrl=$fileUrl"); SCLoadingManager.hide(); SCNavigatorUtils.goBack(context); await Future.delayed(const Duration(milliseconds: 100)); widget.onUpLoadCallBack?.call(true, fileUrl); - } catch (e) { + } catch (e, stackTrace) { + debugPrint( + "[Room Cover Upload] upload failed path=${file.path} error=$e", + ); + debugPrint("[Room Cover Upload] stackTrace=$stackTrace"); SCTts.show("upload fail $e"); SCLoadingManager.hide(); } diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index e7edbea..a02f496 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -934,8 +934,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { Future loadRoomInfo(String roomId) async { try { + debugPrint("[Room Cover Sync] loadRoomInfo start roomId=$roomId"); final value = await SCChatRoomRepository().specific(roomId); final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; + debugPrint( + "[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomName=${value.roomName ?? ""}", + ); currenRoom = currenRoom?.copyWith( roomProfile: currenRoom?.roomProfile?.copyWith( roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter, @@ -956,6 +960,9 @@ class RealTimeCommunicationManager extends ChangeNotifier { ), ), ); + debugPrint( + "[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""}", + ); ///如果是游客禁止上麦,已经在麦上的游客需要下麦 bool touristMike = diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 8b42ff5..a0cdc30 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -784,6 +784,11 @@ class RealTimeMessagingManager extends ChangeNotifier { // 发送消息(房间) Future _sendRoomMessage(Msg msg) async { try { + if (msg.type == SCRoomMsgType.roomSettingUpdate) { + debugPrint( + "[Room Cover Sync] send roomSettingUpdate groupId=${msg.groupId ?? ""} roomId=${msg.msg ?? ""}", + ); + } var user = msg.user?.copyWith(); var toUser = msg.toUser?.copyWith(); user?.cleanWearHonor(); @@ -1073,6 +1078,9 @@ class RealTimeMessagingManager extends ChangeNotifier { } if (msg.type == SCRoomMsgType.roomSettingUpdate) { + debugPrint( + "[Room Cover Sync] recv roomSettingUpdate groupId=$groupID roomId=${msg.msg ?? ""}", + ); Provider.of( context!, listen: false, diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 88f16cf..a4027d5 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart'; import 'package:yumi/shared/business_logic/models/res/sc_gold_record_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_list_res.dart'; @@ -239,19 +240,22 @@ class SCAccountRepository implements SocialChatUserRepository { String roomDesc, String event, ) async { + final payload = { + "id": roomId, + "roomId": roomId, + "roomCover": roomCover, + "cover": roomCover, + "roomName": roomName, + "roomDesc": roomDesc, + "event": event, + }; + debugPrint("[Room Cover Save] request payload=$payload"); final result = await http.put( "1e41384e55cbd6c2374608b129e2ed27", - data: { - "id": roomId, - "roomId": roomId, - "roomCover": roomCover, - "cover": roomCover, - "roomName": roomName, - "roomDesc": roomDesc, - "event": event, - }, + data: payload, fromJson: (json) => SCEditRoomInfoRes.fromJson(json), ); + debugPrint("[Room Cover Save] response=${result.toJson()}"); await SCRoomProfileCache.saveRoomProfile( roomId: roomId, roomCover: roomCover, diff --git a/lib/ui_kit/widgets/room/room_game_bottom_sheet.dart b/lib/ui_kit/widgets/room/room_game_bottom_sheet.dart new file mode 100644 index 0000000..0eae7cc --- /dev/null +++ b/lib/ui_kit/widgets/room/room_game_bottom_sheet.dart @@ -0,0 +1,220 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; + +class RoomGameEntryButton extends StatelessWidget { + const RoomGameEntryButton({super.key}); + + @override + Widget build(BuildContext context) { + return SCDebounceWidget( + onTap: () { + showBottomInBottomDialog( + context, + const RoomGameBottomSheet(), + barrierColor: Colors.black54, + ); + }, + child: Container( + width: 44.w, + height: 44.w, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xff09372E).withValues(alpha: 0.72), + border: Border.all( + color: Colors.white.withValues(alpha: 0.22), + width: 1.w, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.18), + blurRadius: 10.w, + offset: Offset(0, 4.w), + ), + ], + ), + child: Text( + // 当前先用代码绘制的 `🎮` 作为语言房游戏入口占位, + // 后续设计图到位后直接替换成正式 UI 图片资源即可。 + '🎮', + style: TextStyle(fontSize: 22.sp), + ), + ), + ); + } +} + +class RoomGameBottomSheet extends StatelessWidget { + const RoomGameBottomSheet({super.key}); + + static const int _itemsPerRow = 5; + + /// 静态占位数据,后续接入真实接口后直接替换这里的数据源即可。 + static const List<_RoomGameItem> _mockGames = [ + _RoomGameItem(name: 'Ludo', icon: Icons.casino_outlined), + _RoomGameItem(name: 'Dice', icon: Icons.casino), + _RoomGameItem(name: 'UNO', icon: Icons.style_outlined), + _RoomGameItem(name: 'Quiz', icon: Icons.psychology_outlined), + _RoomGameItem(name: 'Race', icon: Icons.sports_motorsports_outlined), + _RoomGameItem(name: 'Poker', icon: Icons.style), + _RoomGameItem(name: 'Lucky', icon: Icons.auto_awesome_outlined), + _RoomGameItem(name: 'Bingo', icon: Icons.grid_on_outlined), + _RoomGameItem(name: '2048', icon: Icons.apps_outlined), + _RoomGameItem(name: 'Chess', icon: Icons.extension_outlined), + _RoomGameItem(name: 'Keno', icon: Icons.confirmation_number_outlined), + _RoomGameItem(name: 'Cards', icon: Icons.view_carousel_outlined), + _RoomGameItem(name: 'Darts', icon: Icons.gps_fixed), + _RoomGameItem(name: 'Puzzle', icon: Icons.interests_outlined), + _RoomGameItem(name: 'Slots', icon: Icons.local_activity_outlined), + _RoomGameItem(name: 'Spin', icon: Icons.blur_circular_outlined), + _RoomGameItem(name: 'Snake', icon: Icons.route_outlined), + _RoomGameItem(name: 'Party', icon: Icons.celebration_outlined), + _RoomGameItem(name: 'Hero', icon: Icons.shield_outlined), + _RoomGameItem(name: 'Arena', icon: Icons.sports_esports_outlined), + ]; + + @override + Widget build(BuildContext context) { + final rowCount = (_mockGames.length / _itemsPerRow).ceil(); + + return SafeArea( + top: false, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(18.w), + topRight: Radius.circular(18.w), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight / 3, + color: const Color(0xff09372E).withValues(alpha: 0.88), + child: Column( + children: [ + SizedBox(height: 10.w), + Container( + width: 34.w, + height: 4.w, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.28), + borderRadius: BorderRadius.circular(99.w), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(16.w, 14.w, 16.w, 10.w), + child: Row( + children: [ + text( + 'Games', + fontSize: 16, + fontWeight: FontWeight.w700, + textColor: Colors.white, + ), + const Spacer(), + text( + 'Static mock data', + fontSize: 11, + textColor: Colors.white70, + ), + ], + ), + ), + Expanded( + child: ListView.builder( + padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 20.w), + itemCount: rowCount, + itemBuilder: (context, rowIndex) { + return Padding( + padding: EdgeInsets.only(bottom: 12.w), + child: Row( + children: List.generate(_itemsPerRow, (columnIndex) { + final itemIndex = + rowIndex * _itemsPerRow + columnIndex; + return Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: + itemIndex < _mockGames.length + ? _RoomGameTile( + item: _mockGames[itemIndex], + ) + : const SizedBox.shrink(), + ), + ); + }), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _RoomGameTile extends StatelessWidget { + const _RoomGameTile({required this.item}); + + final _RoomGameItem item; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + const Color(0xff18F2B1).withValues(alpha: 0.26), + Colors.white.withValues(alpha: 0.08), + ], + ), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1.w, + ), + ), + alignment: Alignment.center, + child: Icon( + // 当前先用系统 Icon 占位,后续会替换成具体游戏 UI 图标/图片资源。 + item.icon, + size: 22.w, + color: Colors.white, + ), + ), + SizedBox(height: 6.w), + text( + item.name, + fontSize: 11, + textColor: Colors.white, + fontWeight: FontWeight.w500, + textAlign: TextAlign.center, + maxLines: 1, + ), + ], + ); + } +} + +class _RoomGameItem { + const _RoomGameItem({required this.name, required this.icon}); + + final String name; + final IconData icon; +} diff --git a/lib/ui_kit/widgets/room/room_play_widget.dart b/lib/ui_kit/widgets/room/room_play_widget.dart index 3f74341..aed4762 100644 --- a/lib/ui_kit/widgets/room/room_play_widget.dart +++ b/lib/ui_kit/widgets/room/room_play_widget.dart @@ -1,60 +1,33 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/ui_kit/widgets/room/room_banner_view.dart'; -import '../../../modules/index/main_route.dart'; -import '../../../services/audio/rtm_manager.dart'; - -class RoomPlayWidget extends StatefulWidget { - @override - _RoomPlayWidgetState createState() => _RoomPlayWidgetState(); -} - -class _RoomPlayWidgetState extends State { - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - alignment: Alignment.bottomCenter, - child: SingleChildScrollView( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - children: [ - Container( - height: 56.w, - margin: EdgeInsets.symmetric(vertical: 5.w), - child: RoomBannerView(), - ), - SizedBox(height: 10.w), - ], - ), - SizedBox(width: 10.w), - ], - ), - ), - ), - ], - ), - ), - ], - ); - ; - }, - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/widgets/room/room_banner_view.dart'; +import 'package:yumi/ui_kit/widgets/room/room_game_bottom_sheet.dart'; + +class RoomPlayWidget extends StatefulWidget { + const RoomPlayWidget({super.key}); + + @override + State createState() => _RoomPlayWidgetState(); +} + +class _RoomPlayWidgetState extends State { + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Stack( + children: [ + PositionedDirectional( + end: 15.w, + bottom: 100.w, + child: const RoomGameEntryButton(), + ), + PositionedDirectional( + end: 10.w, + bottom: 15.w, + child: SizedBox(width: 46.w, child: RoomBannerView()), + ), + ], + ), + ); + } +} diff --git a/需求进度.md b/需求进度.md index ecebb94..9705d9d 100644 --- a/需求进度.md +++ b/需求进度.md @@ -4,6 +4,9 @@ - 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 ## 已完成模块 +- 已为语言房页面补充右下方 `game` 悬浮入口:入口位于聊天区右下侧、距右侧约 `15.w`、位于底部操作栏上方约 `100.w`,当前先使用代码绘制的 `🎮` 占位图标,并已在实现处标注后续替换为正式 UI 图片资源。 +- 已新增语言房游戏底部弹窗:点击 `game` 入口后会从底部弹出约三分之一屏高的面板,采用房间页现有半透明磨砂风格,便于后续继续沿用当前视觉体系。 +- 已补齐语言房游戏列表占位结构:弹窗内部先用静态游戏数据驱动,并按“`ListView` 可上下滑动 + 每行 `5` 个图标位”的方式实现,后续只需替换入口图、列表图标和真实接口数据即可继续开发。 - 创建并持续维护进度跟踪文件。 - 已继续排查语言房 gift 动画链路:确认送礼后会同时走本地房间消息、滚屏礼物条和大额礼物全局飘屏三条路径,并修复动画管理器在控制器尚未绑定完成时提前消费队列导致后续动画不再播放的问题。 - 已定位并修复语言房礼物飘屏资源引用错误:代码里误写 `sc_icon_gift_flosc_bg` / `sc_icon_luck_gift_flosc_*`,实际资源文件名为 `float`,导致点击送礼后飘屏背景图加载失败,相关动画无法正常显示。 @@ -25,6 +28,9 @@ - 已继续细化首页房卡体验:`My` 板块房卡骨架数量已从 `4` 个补齐到 `6` 个,与首页 Party 首屏保持一致;同时将房卡右下角原本的静态绿色“直播中”图片替换为代码驱动的三段式动态音频条,首页、我的、搜索结果和房间浮窗现都会展示跳动效果,不再依赖静态资源图;最新已把动效调整为“单周期完整峰谷 + A/B/C 错峰起伏”的连续波形,观感更接近直播音频电平。 - 已继续微调首页房卡信息密度:房卡底部房名与在线人数字号已统一调小,和动态音频条更协调;同时已移除 `My` 板块顶部 `my room` 标题及其占位,让房间卡片整体上移,对齐更紧凑。 - 已继续微调首页视觉细节:房卡底栏房名再次缩小并轻微上移,和国旗、动态音频条、人数的中线更一致;首页顶部排行骨架也已简化为“3 个圆形头像 + 1 根文本条”,更贴近真实卡片结构。 +- 已补回首页 Party 顶部 banner 展示链路:顶部现优先显示原有 `explore` banner,并在该分组为空时自动兜底使用首页 `home` banner,避免整块区域消失;同时首屏 banner 空数据加载阶段已补充同风格骨架占位,不再直接塌陷为零高度。 +- 已继续微调首页 Party 顶部 banner 体验:banner 与下方排行组件之间已补齐接近房卡区的纵向间距;当 banner 只有 `1` 张时,会关闭无限轮播与横向回弹,左右滑动不再弹回中间。 +- 已继续收窄首页 Party 顶部 banner 骨架样式:banner 骨架现改为更干净的整块占位,不再在中间叠加长方形白条,首屏观感更接近真实 banner 的整体加载。 - 已优化语言房顶部成员入口的点击范围:右上角在线成员区域现已将左侧头像堆叠区与右侧成员图标统一为同一点击热区,用户点击头像区也可直接打开成员列表页。 - 已继续修复房间封面跨用户不同步问题:当列表或进房返回空封面时,客户端会自动补拉 `room/profile/specific` 详情并缓存真实封面;同时房主保存房间资料后会向房间内其他用户派发资料刷新消息,避免只有自己能看到新封面、其他用户仍显示 `no data`。 - 已修复个人主页编辑页头像上传后无变化的问题:上传成功后会先本地回写新头像地址并立即提交;资料更新接口现同时兼容 `userAvatar/avatar` 字段,且不再被紧接着的旧资料回拉覆盖,返回个人主页后头像会即时更新。