图片加载中占位符更改以及app启动初始帧提前

This commit is contained in:
NIGGER SLAYER 2026-04-15 15:38:03 +08:00
parent 456642ff04
commit 553c37c743
11 changed files with 1206 additions and 1029 deletions

View File

@ -1,381 +1,407 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:fluro/fluro.dart' as fluro; import 'package:fluro/fluro.dart' as fluro;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/shared/tools/sc_keybord_util.dart'; import 'package:yumi/shared/tools/sc_keybord_util.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:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart'; import 'package:provider/single_child_widget.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'app_localizations.dart'; import 'app_localizations.dart';
import 'app/config/app_config.dart'; import 'app/config/app_config.dart';
import 'app/constants/sc_global_config.dart'; import 'app/constants/sc_global_config.dart';
import 'app/constants/sc_screen.dart'; import 'app/constants/sc_screen.dart';
import 'app/routes/sc_routes.dart'; import 'app/routes/sc_routes.dart';
import 'app/routes/sc_lk_application.dart'; import 'app/routes/sc_lk_application.dart';
import 'shared/tools/sc_deep_link_handler.dart'; import 'shared/tools/sc_deep_link_handler.dart';
import 'shared/tools/sc_deviceId_utils.dart'; import 'shared/tools/sc_deviceId_utils.dart';
import 'modules/splash/splash_page.dart'; import 'modules/splash/splash_page.dart';
import 'services/general/sc_app_general_manager.dart'; import 'services/general/sc_app_general_manager.dart';
import 'services/payment/apple_payment_manager.dart'; import 'services/payment/apple_payment_manager.dart';
import 'services/auth/authentication_manager.dart'; import 'services/auth/authentication_manager.dart';
import 'services/gift/gift_animation_manager.dart'; import 'services/gift/gift_animation_manager.dart';
import 'services/gift/gift_system_manager.dart'; import 'services/gift/gift_system_manager.dart';
import 'services/payment/google_payment_manager.dart'; import 'services/payment/google_payment_manager.dart';
import 'services/localization/localization_manager.dart'; import 'services/localization/localization_manager.dart';
import 'services/room/rc_room_manager.dart'; import 'services/room/rc_room_manager.dart';
import 'services/audio/rtc_manager.dart'; import 'services/audio/rtc_manager.dart';
import 'services/audio/rtm_manager.dart'; import 'services/audio/rtm_manager.dart';
import 'services/shop/shop_manager.dart'; import 'services/shop/shop_manager.dart';
import 'services/theme/theme_manager.dart'; import 'services/theme/theme_manager.dart';
import 'services/auth/user_profile_manager.dart'; import 'services/auth/user_profile_manager.dart';
import 'ui_kit/theme/socialchat_theme.dart'; import 'ui_kit/theme/socialchat_theme.dart';
bool _isCrashlyticsReady = false;
void main() async {
void main() async {
// //
AppConfig.initialize(); AppConfig.initialize();
// 2. 使 runZonedGuarded
// 2. 使 runZonedGuarded runZonedGuarded(
runZonedGuarded( () async {
() async { // 1. Widget
// 1. Widget WidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized(); //
// GestureBinding.instance.resamplingEnabled = true;
GestureBinding.instance.resamplingEnabled = true; //
// 1UI await _prepareAppShell();
await _setupA(); _installErrorHandlers();
// 使 //
await _initStore(); runApp(const RootAppWithProviders());
// WidgetsBinding.instance.addPostFrameCallback((_) {
runApp(const RootAppWithProviders()); unawaited(_warmUpDeferredServices());
}, });
// 9. runZonedGuarded },
(error, stackTrace) { // 9. runZonedGuarded
// runZonedGuarded (error, stackTrace) {
FirebaseCrashlytics.instance.recordError(error, stackTrace, fatal: true); unawaited(_recordFatalError(error, stackTrace));
debugPrint('Zoned Error: $error\nStack: $stackTrace'); },
}, );
); }
}
///
/// _initializeEssentialServices Future<void> _prepareAppShell() async {
Future<void> _setupA() async { //
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// Android平台状态栏样式设置
// Android平台状态栏样式设置 if (Platform.isAndroid) {
if (Platform.isAndroid) { SystemChrome.setSystemUIOverlayStyle(
SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(
const SystemUiOverlayStyle( statusBarColor: Colors.transparent,
statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark, ),
), );
); }
}
//
// SCKeybordUtil.init();
SCKeybordUtil.init(); // 使
await _initStore();
// Firebase核心初始化 }
try {
await Firebase.initializeApp(); void _installErrorHandlers() {
if (kDebugMode) { FlutterError.onError = (details) {
debugPrint('Firebase初始化完成'); FlutterError.presentError(details);
} unawaited(_recordFlutterError(details));
} catch (e, stackTrace) { };
debugPrint('Firebase初始化异常: $e\n$stackTrace');
// PlatformDispatcher.instance.onError = (error, stack) {
} unawaited(_recordFatalError(error, stack));
return true;
// Flutter错误拦截器配置 };
FlutterError.onError = (details) { }
FirebaseCrashlytics.instance.recordFlutterFatalError(details);
FlutterError.presentError(details); ///
}; Future<void> _warmUpDeferredServices() async {
try {
// if (Firebase.apps.isEmpty) {
PlatformDispatcher.instance.onError = (error, stack) { await Firebase.initializeApp();
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); }
return true; // _isCrashlyticsReady = true;
}; await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
if (kDebugMode) {
// debugPrint('Firebase/Crashlytics 后台初始化完成');
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); }
} } catch (e, stackTrace) {
debugPrint('Firebase 后台初始化异常: $e\n$stackTrace');
}
/// _initializeStorageWithRetry }
Future<void> _initStore() async {
const maxAttempts = 3; Future<void> _recordFlutterError(FlutterErrorDetails details) async {
int attemptCount = 0; if (!_isCrashlyticsReady) {
bool success = false; debugPrint(
'Flutter Error before Crashlytics ready: ${details.exceptionAsString()}',
while (attemptCount < maxAttempts && !success) { );
attemptCount++; return;
try { }
await DataPersistence.initialize();
success = true; try {
if (kDebugMode) { await FirebaseCrashlytics.instance.recordFlutterFatalError(details);
debugPrint('数据存储初始化成功(尝试次数: $attemptCount'); } catch (e, stackTrace) {
} debugPrint('Crashlytics 记录 Flutter 错误失败: $e\n$stackTrace');
} catch (e) { }
debugPrint('数据存储初始化尝试 $attemptCount 失败: $e'); }
if (attemptCount >= maxAttempts) { Future<void> _recordFatalError(Object error, StackTrace stackTrace) async {
debugPrint('数据存储初始化失败,已达最大重试次数: $maxAttempts'); if (!_isCrashlyticsReady) {
// debugPrint('Unhandled error before Crashlytics ready: $error');
return; debugPrintStack(stackTrace: stackTrace);
} return;
}
// 500ms, 1000ms, 1500ms...
int delayMs = 500 * attemptCount; try {
await Future.delayed(Duration(milliseconds: delayMs)); await FirebaseCrashlytics.instance.recordError(
} error,
} stackTrace,
} fatal: true,
);
/// Provider的根组件 } catch (e, recordStackTrace) {
class RootAppWithProviders extends StatelessWidget { debugPrint('Crashlytics 记录异常失败: $e\n$recordStackTrace');
const RootAppWithProviders({super.key}); }
}
@override
Widget build(BuildContext context) { /// _initializeStorageWithRetry
return MultiProvider(providers: _buildP(), child: const YumiApplication()); Future<void> _initStore() async {
} const maxAttempts = 3;
int attemptCount = 0;
/// Provider列表 - bool success = false;
List<SingleChildWidget> _buildP() {
return [ while (attemptCount < maxAttempts && !success) {
// Provider - attemptCount++;
ChangeNotifierProvider<SocialChatAuthenticationManager>( try {
lazy: false, await DataPersistence.initialize();
create: (context) => SocialChatAuthenticationManager(), success = true;
), if (kDebugMode) {
ChangeNotifierProvider<SocialChatUserProfileManager>( debugPrint('数据存储初始化成功(尝试次数: $attemptCount');
lazy: false, }
create: (context) => SocialChatUserProfileManager(), } catch (e) {
), debugPrint('数据存储初始化尝试 $attemptCount 失败: $e');
// UI与主题相关Provider - if (attemptCount >= maxAttempts) {
ChangeNotifierProvider<ThemeManager>( debugPrint('数据存储初始化失败,已达最大重试次数: $maxAttempts');
lazy: true, //
create: (context) => ThemeManager(), return;
), }
ChangeNotifierProvider<LocalizationManager>(
lazy: true, // 500ms, 1000ms, 1500ms...
create: (context) => LocalizationManager(), int delayMs = 500 * attemptCount;
), await Future.delayed(Duration(milliseconds: delayMs));
}
// Provider - }
ChangeNotifierProvider<RtcProvider>( }
lazy: true,
create: (context) => RealTimeCommunicationManager(), /// Provider的根组件
), class RootAppWithProviders extends StatelessWidget {
ChangeNotifierProvider<RtmProvider>( const RootAppWithProviders({super.key});
lazy: true,
create: (context) => RealTimeMessagingManager(), @override
), Widget build(BuildContext context) {
return MultiProvider(providers: _buildP(), child: const YumiApplication());
// Provider - }
ChangeNotifierProvider<SocialChatRoomManager>(
lazy: true, /// Provider列表 -
create: (context) => SocialChatRoomManager(), List<SingleChildWidget> _buildP() {
), return [
// Provider -
// Provider - ChangeNotifierProvider<SocialChatAuthenticationManager>(
ChangeNotifierProvider<GiftProvider>( lazy: true,
lazy: true, create: (context) => SocialChatAuthenticationManager(),
create: (context) => SocialChatGiftSystemManager(), ),
), ChangeNotifierProvider<SocialChatUserProfileManager>(
ChangeNotifierProvider<GiftAnimationManager>( lazy: true,
lazy: true, create: (context) => SocialChatUserProfileManager(),
create: (context) => GiftAnimationManager(), ),
),
// UI与主题相关Provider -
// Provider - ChangeNotifierProvider<ThemeManager>(
ChangeNotifierProvider<AndroidPaymentProcessor>( lazy: true,
lazy: true, create: (context) => ThemeManager(),
create: (context) => AndroidPaymentProcessor(), ),
), ChangeNotifierProvider<LocalizationManager>(
ChangeNotifierProvider<IOSPaymentProcessor>( lazy: true,
lazy: true, create: (context) => LocalizationManager(),
create: (context) => IOSPaymentProcessor(), ),
),
ChangeNotifierProvider<ShopManager>( // Provider -
lazy: true, ChangeNotifierProvider<RtcProvider>(
create: (context) => ShopManager(), lazy: true,
), create: (context) => RealTimeCommunicationManager(),
),
ChangeNotifierProvider<RtmProvider>(
// Provider - lazy: true,
ChangeNotifierProvider<SCAppGeneralManager>( create: (context) => RealTimeMessagingManager(),
lazy: true, ),
create: (context) => SCAppGeneralManager(),
), // Provider -
]; ChangeNotifierProvider<SocialChatRoomManager>(
} lazy: true,
} create: (context) => SocialChatRoomManager(),
),
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>(); // Provider -
ChangeNotifierProvider<GiftProvider>(
class YumiApplication extends StatefulWidget { lazy: true,
const YumiApplication({super.key}); create: (context) => SocialChatGiftSystemManager(),
),
@override ChangeNotifierProvider<GiftAnimationManager>(
State<YumiApplication> createState() => _YumiApplicationState(); lazy: true,
} create: (context) => GiftAnimationManager(),
),
class _YumiApplicationState extends State<YumiApplication> {
late fluro.FluroRouter _router; // Provider -
final SCDeepLinkHandler _deepLinkHandler = SCDeepLinkHandler(); ChangeNotifierProvider<AndroidPaymentProcessor>(
lazy: true,
@override create: (context) => AndroidPaymentProcessor(),
void initState() { ),
super.initState(); ChangeNotifierProvider<IOSPaymentProcessor>(
// lazy: true,
_initLink(); create: (context) => IOSPaymentProcessor(),
// ),
WidgetsBinding.instance.addPostFrameCallback((_) { ChangeNotifierProvider<ShopManager>(
_initRouter(); lazy: true,
SCDeviceIdUtils.initDeviceinfo(); create: (context) => ShopManager(),
}); ),
}
// Provider -
@override ChangeNotifierProvider<SCAppGeneralManager>(
dispose() { lazy: true,
_deepLinkHandler.dispose(); create: (context) => SCAppGeneralManager(),
super.dispose(); ),
} ];
}
Future<void> _initLink() async { }
//
await _deepLinkHandler.initDeepLinks( final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
onLinkReceived: (Uri uri) { final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
//
_handleLink(uri); class YumiApplication extends StatefulWidget {
}, const YumiApplication({super.key});
);
} @override
State<YumiApplication> createState() => _YumiApplicationState();
void _handleLink(Uri uri) { }
//
print('App 根层收到链接: $uri'); class _YumiApplicationState extends State<YumiApplication> {
String path = uri.path; late fluro.FluroRouter _router;
String id = uri.queryParameters['id'] ?? ''; final SCDeepLinkHandler _deepLinkHandler = SCDeepLinkHandler();
}
@override
void _initRouter() { void initState() {
_router = fluro.FluroRouter(); super.initState();
SCRoutes.configureRoutes(_router); //
SCLkApplication.router = _router; WidgetsBinding.instance.addPostFrameCallback((_) {
} _initRouter();
SCDeviceIdUtils.initDeviceinfo();
@override unawaited(_initLink());
Widget build(BuildContext context) { });
return _buildUI(); }
}
@override
// dispose() {
Widget Function() _buildFooter() { _deepLinkHandler.dispose();
return () => CustomFooter( super.dispose();
builder: (BuildContext context, LoadStatus? mode) { }
Widget body;
if (mode == LoadStatus.idle) { Future<void> _initLink() async {
body = Text( //
SCAppLocalizations.of(context)!.pullToLoadMore, await _deepLinkHandler.initDeepLinks(
); onLinkReceived: (Uri uri) {
} else if (mode == LoadStatus.loading) { //
body = CupertinoActivityIndicator(color: Colors.white24); _handleLink(uri);
} else if (mode == LoadStatus.failed) { },
body = Text( );
SCAppLocalizations.of(context)!.loadingFailedClickToRetry, }
);
} else if (mode == LoadStatus.canLoading) { void _handleLink(Uri uri) {
body = Text( //
SCAppLocalizations.of(context)!.releaseToLoadMore, debugPrint('App 根层收到链接: $uri');
); }
} else {
body = Text( void _initRouter() {
"", _router = fluro.FluroRouter();
style: TextStyle( SCRoutes.configureRoutes(_router);
fontSize: 12.sp, SCLkApplication.router = _router;
color: Color(0xff999999), }
),
); @override
} Widget build(BuildContext context) {
return Container( return _buildUI();
height: kTextTabBarHeight + ScreenUtil().statusBarHeight, }
child: Center(child: body),
); //
}, Widget Function() _buildFooter() {
); return () => CustomFooter(
} builder: (BuildContext context, LoadStatus? mode) {
Widget body;
if (mode == LoadStatus.idle) {
Widget _buildUI() { body = Text(SCAppLocalizations.of(context)!.pullToLoadMore);
return Consumer<LocalizationManager>( } else if (mode == LoadStatus.loading) {
builder: (context, localeProvider, child) { body = CupertinoActivityIndicator(color: Colors.white24);
// LocaleProvider可用时才设置语言 } else if (mode == LoadStatus.failed) {
SCGlobalConfig.lang = localeProvider.locale?.languageCode ?? "en"; body = Text(
return Consumer<ThemeManager>( SCAppLocalizations.of(context)!.loadingFailedClickToRetry,
builder: (context, themeManager, child) { );
return ScreenUtilInit( } else if (mode == LoadStatus.canLoading) {
designSize: Size(SCScreen.designWidth, SCScreen.designHeight), body = Text(SCAppLocalizations.of(context)!.releaseToLoadMore);
splitScreenMode: false, } else {
minTextAdapt: true, body = Text(
builder: (context, child) { "",
return AnnotatedRegion<SystemUiOverlayStyle>( style: TextStyle(fontSize: 12.sp, color: Color(0xff999999)),
value: SystemUiOverlayStyle.dark, );
child: RefreshConfiguration( }
headerBuilder: () => MaterialClassicHeader(color: SocialChatTheme.primaryColor), return Container(
footerBuilder: _buildFooter(), height: kTextTabBarHeight + ScreenUtil().statusBarHeight,
child: MaterialApp( child: Center(child: body),
title: 'Yumi', );
locale: Provider.of<LocalizationManager>(context).locale, },
localizationsDelegates: [ );
SCAppLocalizations.delegate, }
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, Widget _buildUI() {
GlobalWidgetsLocalizations.delegate, return Consumer<LocalizationManager>(
], builder: (context, localeProvider, child) {
supportedLocales: [ // LocaleProvider可用时才设置语言
const Locale('en', ''), SCGlobalConfig.lang = localeProvider.locale?.languageCode ?? "en";
const Locale('zh', ''), return Consumer<ThemeManager>(
const Locale('ar', ''), builder: (context, themeManager, child) {
const Locale('tr', ''), return ScreenUtilInit(
const Locale('bn', ''), designSize: Size(SCScreen.designWidth, SCScreen.designHeight),
], splitScreenMode: false,
navigatorKey: navigatorKey, minTextAdapt: true,
debugShowCheckedModeBanner: false, builder: (context, child) {
onGenerateRoute: SCLkApplication.router.generator, return AnnotatedRegion<SystemUiOverlayStyle>(
theme: themeManager.currentTheme, value: SystemUiOverlayStyle.dark,
home: SplashPage(), child: RefreshConfiguration(
builder: FlutterSmartDialog.init(), headerBuilder:
navigatorObservers: [routeObserver], () => MaterialClassicHeader(
), color: SocialChatTheme.primaryColor,
), ),
); footerBuilder: _buildFooter(),
}, child: MaterialApp(
); title: 'Yumi',
}, locale: localeProvider.locale,
); localizationsDelegates: const [
}, SCAppLocalizations.delegate,
); GlobalMaterialLocalizations.delegate,
} GlobalCupertinoLocalizations.delegate,
} GlobalWidgetsLocalizations.delegate,
],
supportedLocales: const [
Locale('en', ''),
Locale('zh', ''),
Locale('ar', ''),
Locale('tr', ''),
Locale('bn', ''),
],
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
onGenerateRoute: SCLkApplication.router.generator,
theme: themeManager.currentTheme,
home: const SplashPage(),
builder: FlutterSmartDialog.init(),
navigatorObservers: [routeObserver],
),
),
);
},
);
},
);
},
);
}
}

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
@ -21,14 +22,19 @@ const Duration _kGiftPageSkeletonMinDuration = Duration(milliseconds: 420);
const Duration _kGiftPageSkeletonMaxDuration = Duration(milliseconds: 900); const Duration _kGiftPageSkeletonMaxDuration = Duration(milliseconds: 900);
class GiftTabPage extends StatefulWidget { class GiftTabPage extends StatefulWidget {
String type; final String type;
bool isDark = true; final bool isDark;
Function(int checkedIndex) checkedCall; final Function(int checkedIndex) checkedCall;
GiftTabPage(this.type, this.checkedCall, {super.key, this.isDark = true}); const GiftTabPage(
this.type,
this.checkedCall, {
super.key,
this.isDark = true,
});
@override @override
_GiftTabPageState createState() => _GiftTabPageState(); State<GiftTabPage> createState() => _GiftTabPageState();
} }
class _GiftTabPageState extends State<GiftTabPage> class _GiftTabPageState extends State<GiftTabPage>
@ -202,12 +208,8 @@ class _GiftTabPageState extends State<GiftTabPage>
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Opacity( child: Opacity(
opacity: 0.75, opacity: 0.9,
child: Image.asset( child: SCRotatingDotsLoading(size: 72.w),
"sc_images/general/sc_icon_loading.png",
width: 92.w,
fit: BoxFit.contain,
),
), ),
), ),
), ),
@ -245,12 +247,8 @@ class _GiftTabPageState extends State<GiftTabPage>
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
color: baseColor, color: baseColor,
image: const DecorationImage(
image: AssetImage("sc_images/general/sc_icon_loading.png"),
fit: BoxFit.cover,
opacity: 0.32,
),
), ),
child: Center(child: SCRotatingDotsLoading(size: 24.w)),
), ),
SizedBox(height: 7.w), SizedBox(height: 7.w),
Container( Container(
@ -276,6 +274,19 @@ class _GiftTabPageState extends State<GiftTabPage>
} }
Widget _buildGiftCoverLoading() { Widget _buildGiftCoverLoading() {
return Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.w),
color: Colors.white.withValues(alpha: 0.08),
),
alignment: Alignment.center,
child: SCRotatingDotsLoading(size: 24.w),
);
}
Widget _buildGiftCoverNoData() {
return Container( return Container(
width: 48.w, width: 48.w,
height: 48.w, height: 48.w,
@ -424,7 +435,7 @@ class _GiftTabPageState extends State<GiftTabPage>
width: 48.w, width: 48.w,
height: 48.w, height: 48.w,
loadingWidget: _buildGiftCoverLoading(), loadingWidget: _buildGiftCoverLoading(),
errorWidget: _buildGiftCoverLoading(), errorWidget: _buildGiftCoverNoData(),
), ),
SizedBox(height: 5.w), SizedBox(height: 5.w),
Container( Container(

View File

@ -12,8 +12,10 @@ import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
///Home页面 ///Home页面
class SCIndexHomePage extends StatefulWidget { class SCIndexHomePage extends StatefulWidget {
const SCIndexHomePage({super.key});
@override @override
_SCIndexHomePageState createState() => _SCIndexHomePageState(); State<SCIndexHomePage> createState() => _SCIndexHomePageState();
} }
class _SCIndexHomePageState extends State<SCIndexHomePage> class _SCIndexHomePageState extends State<SCIndexHomePage>
@ -21,38 +23,44 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
TabController? _tabController; TabController? _tabController;
final List<Widget> _pages = []; final List<Widget> _pages = [];
final List<Widget> _tabs = []; final List<Widget> _tabs = [];
bool refresh = false; // 👈 false Locale? _lastLocale;
@override @override
void initState() { void didChangeDependencies() {
super.initState(); super.didChangeDependencies();
// final locale = Localizations.localeOf(context);
WidgetsBinding.instance.addPostFrameCallback((_) { if (_lastLocale == locale && _tabController != null && _pages.isNotEmpty) {
_updateTabs(); return;
}); }
_lastLocale = locale;
_rebuildTabs();
} }
void _updateTabs() { void _rebuildTabs() {
_tabController?.dispose();
_pages.clear();
final strategy = SCGlobalConfig.businessLogicStrategy; final strategy = SCGlobalConfig.businessLogicStrategy;
_pages.addAll(strategy.getHomeTabPages(context)); final nextPages = strategy.getHomeTabPages(context);
final nextTabs = strategy.getHomeTabLabels(context);
if (nextPages.isEmpty || nextTabs.length != nextPages.length) {
return;
}
final desiredIndex =
_tabController?.index ?? strategy.getHomeInitialTabIndex();
final initialIndex = desiredIndex.clamp(0, nextPages.length - 1);
_tabController?.dispose();
_pages
..clear()
..addAll(nextPages);
_tabs
..clear()
..addAll(nextTabs);
_tabController = TabController( _tabController = TabController(
length: _pages.length, length: _pages.length,
vsync: this, vsync: this,
initialIndex: strategy.getHomeInitialTabIndex(), initialIndex: initialIndex,
); );
//
_tabController?.addListener(() {});
//
if (mounted) {
setState(() {
refresh = true;
});
}
} }
@override @override
@ -63,8 +71,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// if (_tabController == null || _pages.isEmpty || _tabs.isEmpty) {
if (!refresh || _tabController == null || _pages.isEmpty) {
return Center( return Center(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -78,11 +85,6 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
); );
} }
//
_tabs.clear();
final strategy = SCGlobalConfig.businessLogicStrategy;
_tabs.addAll(strategy.getHomeTabLabels(context));
return Stack( return Stack(
children: [ children: [
Scaffold( Scaffold(
@ -100,7 +102,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
tabAlignment: TabAlignment.start, tabAlignment: TabAlignment.start,
isScrollable: true, isScrollable: true,
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
overlayColor: MaterialStateProperty.all( overlayColor: WidgetStateProperty.all(
Colors.transparent, Colors.transparent,
), ),
indicator: const BoxDecoration(), indicator: const BoxDecoration(),
@ -108,12 +110,12 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
fontSize: 19.sp, fontSize: 19.sp,
color:SocialChatTheme.primaryLight, color: SocialChatTheme.primaryLight,
), ),
unselectedLabelStyle: TextStyle( unselectedLabelStyle: TextStyle(
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.white color: Colors.white,
), ),
indicatorColor: Colors.transparent, indicatorColor: Colors.transparent,
dividerColor: Colors.transparent, dividerColor: Colors.transparent,

View File

@ -1,267 +1,288 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
import 'package:yumi/modules/home/index_home_page.dart'; import 'package:yumi/modules/home/index_home_page.dart';
import 'package:yumi/modules/chat/message/sc_message_page.dart'; import 'package:yumi/modules/chat/message/sc_message_page.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:provider/provider.dart'; 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/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.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 '../../shared/tools/sc_heartbeat_utils.dart'; import '../../shared/tools/sc_heartbeat_utils.dart';
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
import '../../ui_kit/components/sc_float_ichart.dart'; import '../../ui_kit/components/sc_float_ichart.dart';
import '../home/popular/event/home_event_page.dart'; import '../home/popular/event/home_event_page.dart';
import '../user/me_page2.dart'; import '../user/me_page2.dart';
/** /**
* *
*/ */
class SCIndexPage extends StatefulWidget { class SCIndexPage extends StatefulWidget {
@override const SCIndexPage({super.key});
_SCIndexPageState createState() => _SCIndexPageState();
} @override
State<SCIndexPage> createState() => _SCIndexPageState();
class _SCIndexPageState extends State<SCIndexPage> { }
int _currentIndex = 0;
final List<Widget> _pages = []; class _SCIndexPageState extends State<SCIndexPage> {
final List<BottomNavigationBarItem> _bottomItems = []; int _currentIndex = 0;
SCAppGeneralManager? generalProvider; final List<Widget> _pages = [];
final List<BottomNavigationBarItem> _bottomItems = [];
@override SCAppGeneralManager? generalProvider;
void initState() { Locale? _lastLocale;
super.initState();
generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false); @override
Provider.of<RtcProvider>( void initState() {
context, super.initState();
listen: false, _initializePages();
).initializeRealTimeCommunicationManager(context); generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false);
Provider.of<RtmProvider>(context, listen: false).init(context); Provider.of<RtcProvider>(
Provider.of<SocialChatUserProfileManager>( context,
context, listen: false,
listen: false, ).initializeRealTimeCommunicationManager(context);
).getUserIdentity(); Provider.of<RtmProvider>(context, listen: false).init(context);
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance(); Provider.of<SocialChatUserProfileManager>(
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); context,
DataPersistence.setLang(SCGlobalConfig.lang); listen: false,
OverlayManager().activate(); ).getUserIdentity();
WakelockPlus.enable(); Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
String roomId = DataPersistence.getLastTimeRoomId(); SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
if (roomId.isNotEmpty) { DataPersistence.setLang(SCGlobalConfig.lang);
SCAccountRepository().quitRoom(roomId).catchError((e) { OverlayManager().activate();
return true; // WidgetsBinding.instance.addPostFrameCallback((_) {
}); WakelockPlus.enable();
} });
} String roomId = DataPersistence.getLastTimeRoomId();
if (roomId.isNotEmpty) {
@override SCAccountRepository().quitRoom(roomId).catchError((e) {
void dispose() { return true; //
WakelockPlus.disable(); });
super.dispose(); }
} }
@override @override
Widget build(BuildContext context) { void dispose() {
SCFloatIchart().init(context); WakelockPlus.disable();
_initWidget(); super.dispose();
return WillPopScope( }
onWillPop: _doubleExit,
child: Stack( @override
children: [ void didChangeDependencies() {
Image.asset( super.didChangeDependencies();
"sc_images/index/sc_icon_index_bg.png", SCFloatIchart().init(context);
width: ScreenUtil().screenWidth, _rebuildBottomItemsIfNeeded();
height: ScreenUtil().screenHeight, }
fit: BoxFit.fill,
), @override
SafeArea( Widget build(BuildContext context) {
top: false, return WillPopScope(
child: Scaffold( onWillPop: _doubleExit,
resizeToAvoidBottomInset: false, child: Stack(
backgroundColor: Colors.transparent, children: [
body: _pages[_currentIndex], Image.asset(
bottomNavigationBar: Container( "sc_images/index/sc_icon_index_bg.png",
height: 85.w, width: ScreenUtil().screenWidth,
decoration: BoxDecoration( height: ScreenUtil().screenHeight,
image: DecorationImage( fit: BoxFit.fill,
image: AssetImage( ),
"sc_images/index/sc_index_bottom_navigation_bar_bg.png", SafeArea(
), top: false,
fit: BoxFit.fill, child: Scaffold(
), resizeToAvoidBottomInset: false,
), backgroundColor: Colors.transparent,
child: Theme( body: _pages[_currentIndex],
data: Theme.of(context).copyWith( bottomNavigationBar: Container(
splashFactory: NoSplash.splashFactory, height: 85.w,
splashColor: Colors.transparent, decoration: BoxDecoration(
highlightColor: Colors.transparent, image: DecorationImage(
hoverColor: Colors.transparent, image: AssetImage(
), "sc_images/index/sc_index_bottom_navigation_bar_bg.png",
child: BottomNavigationBar( ),
elevation: 0, fit: BoxFit.fill,
enableFeedback: false, ),
backgroundColor: Colors.transparent, ),
selectedLabelStyle: TextStyle( child: Theme(
fontWeight: FontWeight.w600, data: Theme.of(context).copyWith(
fontSize: 14.sp, splashFactory: NoSplash.splashFactory,
), splashColor: Colors.transparent,
unselectedLabelStyle: TextStyle( highlightColor: Colors.transparent,
fontWeight: FontWeight.w500, hoverColor: Colors.transparent,
fontSize: 13.sp, ),
), child: BottomNavigationBar(
type: BottomNavigationBarType.fixed, elevation: 0,
selectedItemColor: Color(0xffBF854A), enableFeedback: false,
unselectedItemColor: Color(0xffC4C4C4), backgroundColor: Colors.transparent,
showUnselectedLabels: true, selectedLabelStyle: TextStyle(
showSelectedLabels: true, fontWeight: FontWeight.w600,
items: _bottomItems, fontSize: 14.sp,
currentIndex: _currentIndex, ),
onTap: (index) => setState(() => _currentIndex = index), unselectedLabelStyle: TextStyle(
), fontWeight: FontWeight.w500,
), fontSize: 13.sp,
), ),
), type: BottomNavigationBarType.fixed,
), selectedItemColor: Color(0xffBF854A),
], unselectedItemColor: Color(0xffC4C4C4),
), showUnselectedLabels: true,
); showSelectedLabels: true,
} items: _bottomItems,
currentIndex: _currentIndex,
///退 onTap: (index) => setState(() => _currentIndex = index),
int _lastClickTime = 0; ),
),
Future<bool> _doubleExit() { ),
int nowTime = DateTime.now().microsecondsSinceEpoch; ),
if (_lastClickTime != 0 && nowTime - _lastClickTime > 1500) { ),
return Future.value(true); ],
} else { ),
_lastClickTime = DateTime.now().microsecondsSinceEpoch; );
Future.delayed(const Duration(milliseconds: 1500), () { }
_lastClickTime = 0;
}); ///退
return Future.value(false); int _lastClickTime = 0;
}
} Future<bool> _doubleExit() {
int nowTime = DateTime.now().microsecondsSinceEpoch;
void _initWidget() { if (_lastClickTime != 0 && nowTime - _lastClickTime > 1500) {
_pages.clear(); return Future.value(true);
_bottomItems.clear(); } else {
_pages.add(SCIndexHomePage()); _lastClickTime = DateTime.now().microsecondsSinceEpoch;
_pages.add(HomeEventPage()); Future.delayed(const Duration(milliseconds: 1500), () {
// _pages.add(IndexDynamicPage()); _lastClickTime = 0;
_pages.add(SCMessagePage()); });
_pages.add(MePage2()); return Future.value(false);
}
_bottomItems.add( }
BottomNavigationBarItem(
icon: Image.asset( void _initializePages() {
"sc_images/index/sc_icon_home_no.png", if (_pages.isNotEmpty) {
width: 35.w, return;
height: 35.w, }
),
activeIcon: Image.asset( _pages.add(SCIndexHomePage());
"sc_images/index/sc_icon_home_en.png", _pages.add(HomeEventPage());
width: 35.w, _pages.add(SCMessagePage());
height: 35.w, _pages.add(MePage2());
), }
label: SCAppLocalizations.of(context)!.home,
), void _rebuildBottomItemsIfNeeded() {
); final locale = Localizations.localeOf(context);
_bottomItems.add( if (_lastLocale == locale && _bottomItems.isNotEmpty) {
BottomNavigationBarItem( return;
icon: Image.asset( }
"sc_images/index/sc_icon_home_no.png", _lastLocale = locale;
width: 35.w, _bottomItems.clear();
height: 35.w,
), _bottomItems.add(
activeIcon: Image.asset( BottomNavigationBarItem(
"sc_images/index/sc_icon_home_en.png", icon: Image.asset(
width: 35.w, "sc_images/index/sc_icon_home_no.png",
height: 35.w, width: 35.w,
), height: 35.w,
label: SCAppLocalizations.of(context)!.explore, ),
), activeIcon: Image.asset(
); "sc_images/index/sc_icon_home_en.png",
width: 35.w,
_bottomItems.add( height: 35.w,
BottomNavigationBarItem( ),
icon: Selector<RtmProvider, int>( label: SCAppLocalizations.of(context)!.home,
selector: (c, p) => p.allUnReadCount, ),
shouldRebuild: (prev, next) => prev != next, );
builder: (_, allUnReadCount, __) { _bottomItems.add(
return allUnReadCount > 0 BottomNavigationBarItem(
? Badge( icon: Image.asset(
backgroundColor: Colors.red, "sc_images/index/sc_icon_home_no.png",
label: text( width: 35.w,
"${allUnReadCount > 99 ? "99+" : allUnReadCount}", height: 35.w,
fontSize: 9.sp, ),
textColor: Colors.white, activeIcon: Image.asset(
fontWeight: FontWeight.w600, "sc_images/index/sc_icon_home_en.png",
), width: 35.w,
alignment: AlignmentDirectional.topEnd, height: 35.w,
child: Image.asset( ),
"sc_images/index/sc_icon_message_no.png", label: SCAppLocalizations.of(context)!.explore,
width: 35.w, ),
height: 35.w, );
),
) _bottomItems.add(
: Image.asset( BottomNavigationBarItem(
"sc_images/index/sc_icon_message_no.png", icon: Selector<RtmProvider, int>(
width: 35.w, selector: (c, p) => p.allUnReadCount,
height: 35.w, shouldRebuild: (prev, next) => prev != next,
); builder: (_, allUnReadCount, __) {
}, return allUnReadCount > 0
), ? Badge(
activeIcon: Selector<RtmProvider, int>( backgroundColor: Colors.red,
selector: (c, p) => p.allUnReadCount, label: text(
shouldRebuild: (prev, next) => prev != next, "${allUnReadCount > 99 ? "99+" : allUnReadCount}",
builder: (_, allUnReadCount, __) { fontSize: 9.sp,
return allUnReadCount > 0 textColor: Colors.white,
? Badge( fontWeight: FontWeight.w600,
backgroundColor: Colors.red, ),
label: text( alignment: AlignmentDirectional.topEnd,
"${allUnReadCount > 99 ? "99+" : allUnReadCount}", child: Image.asset(
fontSize: 9.sp, "sc_images/index/sc_icon_message_no.png",
textColor: Colors.white, width: 35.w,
fontWeight: FontWeight.w600, height: 35.w,
), ),
alignment: AlignmentDirectional.topEnd, )
child: Image.asset( : Image.asset(
"sc_images/index/sc_icon_message_en.png", "sc_images/index/sc_icon_message_no.png",
width: 35.w, width: 35.w,
height: 35.w, height: 35.w,
), );
) },
: Image.asset( ),
"sc_images/index/sc_icon_message_en.png", activeIcon: Selector<RtmProvider, int>(
width: 35.w, selector: (c, p) => p.allUnReadCount,
height: 35.w, shouldRebuild: (prev, next) => prev != next,
); builder: (_, allUnReadCount, __) {
}, return allUnReadCount > 0
), ? Badge(
label: SCAppLocalizations.of(context)!.message, backgroundColor: Colors.red,
), label: text(
); "${allUnReadCount > 99 ? "99+" : allUnReadCount}",
_bottomItems.add( fontSize: 9.sp,
BottomNavigationBarItem( textColor: Colors.white,
icon: Image.asset( fontWeight: FontWeight.w600,
"sc_images/index/sc_icon_me_no.png", ),
width: 35.w, alignment: AlignmentDirectional.topEnd,
height: 35.w, child: Image.asset(
), "sc_images/index/sc_icon_message_en.png",
activeIcon: Image.asset( width: 35.w,
"sc_images/index/sc_icon_me_en.png", height: 35.w,
width: 35.w, ),
height: 35.w, )
), : Image.asset(
label: SCAppLocalizations.of(context)!.me, "sc_images/index/sc_icon_message_en.png",
), width: 35.w,
); height: 35.w,
} );
} },
),
label: SCAppLocalizations.of(context)!.message,
),
);
_bottomItems.add(
BottomNavigationBarItem(
icon: Image.asset(
"sc_images/index/sc_icon_me_no.png",
width: 35.w,
height: 35.w,
),
activeIcon: Image.asset(
"sc_images/index/sc_icon_me_en.png",
width: 35.w,
height: 35.w,
),
label: SCAppLocalizations.of(context)!.me,
),
);
}
}

View File

@ -1,121 +1,96 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/material.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/shared/tools/sc_version_utils.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/tools/sc_version_utils.dart'; import 'package:yumi/app/routes/sc_routes.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/app/routes/sc_routes.dart'; import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/modules/auth/login_route.dart';
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart';
import 'package:yumi/modules/auth/login_route.dart';
class SplashPage extends StatefulWidget {
@override
_SplashPageState createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
Timer? _timer;
int countdown = 6;
int status = 0;
SCStartPageRes? currentStartPage;
List<SCStartPageRes> startPages = [];
@override
void initState() {
super.initState();
FileCacheManager.getInstance().getFilePath();
getStartPage();
_startTimer(context);
}
///
void getStartPage() {
SCConfigRepositoryImp().getStartPage().then((result) {
startPages = result;
if (startPages.length > 1) {
int index = Random().nextInt(startPages.length);
currentStartPage = result[index];
} else if (startPages.isNotEmpty) {
currentStartPage = result.first;
} else {
currentStartPage = null;
}
status = 1; class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
static const Duration _minimumSplashDuration = Duration(milliseconds: 900);
Timer? _timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(FileCacheManager.getInstance().getFilePath());
});
_startNavigationTimer();
}
@override
void dispose() {
_cancelTimer();
super.dispose();
}
///
void _startNavigationTimer() {
_timer = Timer(_minimumSplashDuration, () {
if (mounted) { if (mounted) {
setState(() {}); _goMainPage();
} }
}); });
} }
@override @override
void dispose() { Widget build(BuildContext context) {
_cancelTimer(); return Material(
super.dispose(); child: Stack(
} alignment: Alignment.center,
children: [
/// Image.asset(
void _startTimer(BuildContext context) { SCGlobalConfig.businessLogicStrategy.getSplashPageBackgroundImage(),
// `Timer``periodic` width: ScreenUtil().screenWidth,
_timer = Timer.periodic(Duration(seconds: 1), (timer) { height: ScreenUtil().screenHeight,
if (countdown == 0) { fit: BoxFit.cover,
_cancelTimer(); ),
_goMainPage(context); Image.asset(
return; SCGlobalConfig.businessLogicStrategy.getSplashPageIcon(),
} width: 107.w,
countdown--; height: 159.w,
setState(() {}); ),
}); ],
} ),
);
@override }
Widget build(BuildContext context) {
return Material( void _goMainPage() async {
child: Stack( try {
alignment: Alignment.center, await SCVersionUtils.checkReview();
children: [ if (!mounted) {
Image.asset( return;
SCGlobalConfig.businessLogicStrategy.getSplashPageBackgroundImage(), }
width: ScreenUtil().screenWidth, var user = AccountStorage().getCurrentUser();
height: ScreenUtil().screenHeight, var token = AccountStorage().getToken();
fit: BoxFit.cover, if (user != null && token.isNotEmpty) {
), SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
Image.asset( } else {
SCGlobalConfig.businessLogicStrategy.getSplashPageIcon(), SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
width: 107.w, }
height: 159.w, } catch (e) {
), if (!mounted) {
], return;
), }
); SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
} }
}
void _goMainPage(BuildContext context) async {
try { ///
await SCVersionUtils.checkReview(); void _cancelTimer() {
var user = AccountStorage().getCurrentUser(); // `Timer``cancel`
var token = AccountStorage().getToken(); _timer?.cancel();
if (user != null && token.isNotEmpty) { }
SCNavigatorUtils.push(context, SCRoutes.home, replace: true); }
} else {
SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
}
} catch (e) {
SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
}
}
///
void _cancelTimer() {
// `Timer``cancel`
_timer?.cancel();
}
}

View File

@ -1,54 +1,59 @@
import 'dart:ui' as ui; import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
class LocalizationManager with ChangeNotifier {
Locale? _locale; class LocalizationManager with ChangeNotifier {
Locale? _locale;
Locale? get locale => _locale;
Locale? get locale => _locale;
LocalizationManager() {
initializeLanguageSettings(); LocalizationManager() {
} _locale = _resolveInitialLocale();
if (_locale != null) {
initializeLanguageSettings() { SCGlobalConfig.lang = _locale!.languageCode;
String language = DataPersistence.getLang(); }
if (language.isEmpty) { }
Locale systemLocale = ui.window.locale;
if (systemLocale.languageCode.startsWith("ar")) { Locale _resolveInitialLocale() {
_locale = Locale('ar', ''); String language = DataPersistence.getLang();
} else if (systemLocale.languageCode == "en") { if (language.isEmpty) {
_locale = Locale('en', ''); Locale systemLocale = ui.PlatformDispatcher.instance.locale;
} else if (systemLocale.languageCode == "tr") { if (systemLocale.languageCode.startsWith("ar")) {
_locale = Locale('tr', ''); return const Locale('ar', '');
} else if (systemLocale.languageCode == "bn") { } else if (systemLocale.languageCode == "en") {
_locale = Locale('bn', ''); return const Locale('en', '');
} else { } else if (systemLocale.languageCode == "tr") {
_locale = Locale('en', ''); return const Locale('tr', '');
} } else if (systemLocale.languageCode == "bn") {
} else { return const Locale('bn', '');
if (language.startsWith("ar")) { } else {
_locale = Locale('ar', ''); return const Locale('en', '');
} else if (language == "tr") { }
_locale = Locale('tr', ''); } else {
} else if (language == "bn") { if (language.startsWith("ar")) {
_locale = Locale('bn', ''); return const Locale('ar', '');
} else { } else if (language == "tr") {
_locale = Locale('en', ''); return const Locale('tr', '');
} } else if (language == "bn") {
} return const Locale('bn', '');
if (_locale != null) { } else {
changeAppLanguage(_locale!); return const Locale('en', '');
} }
} }
}
void changeAppLanguage(Locale locale) {
_locale = locale; void changeAppLanguage(Locale locale) {
SCGlobalConfig.lang = locale.languageCode; if (_locale?.languageCode == locale.languageCode &&
DataPersistence.setLang(locale.languageCode); _locale?.countryCode == locale.countryCode) {
notifyListeners(); return;
} }
} _locale = locale;
SCGlobalConfig.lang = locale.languageCode;
unawaited(DataPersistence.setLang(locale.languageCode));
notifyListeners();
}
}

View File

@ -1,101 +1,108 @@
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:google_sign_in/google_sign_in.dart'; import 'package:flutter/foundation.dart';
import 'package:google_sign_in/google_sign_in.dart';
///
class SCGoogleAuthService { ///
static final GoogleSignIn _g = GoogleSignIn(scopes: ['email', 'profile'],); class SCGoogleAuthService {
static final GoogleSignIn _g = GoogleSignIn(scopes: ['email', 'profile']);
//
static User? get currentUser => FirebaseAuth.instance.currentUser; //
static User? get currentUser => FirebaseAuth.instance.currentUser;
//
static bool get isSignedIn => currentUser != null; //
static bool get isSignedIn => currentUser != null;
// Firebase
static Future<void> initialize() async { // Firebase
await Firebase.initializeApp(); static Future<void> initialize() async {
} if (Firebase.apps.isNotEmpty) {
return;
// }
static Future<User?> signInWithGoogle() async { await Firebase.initializeApp();
try { }
//
final GoogleSignInAccount? googleAccount = await _g.signIn(); //
if (googleAccount == null) return null; static Future<User?> signInWithGoogle() async {
try {
// await initialize();
final GoogleSignInAuthentication googleAuth = await googleAccount.authentication; //
final GoogleSignInAccount? googleAccount = await _g.signIn();
// Firebase凭证 if (googleAccount == null) return null;
final OAuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken, //
idToken: googleAuth.idToken, final GoogleSignInAuthentication googleAuth =
); await googleAccount.authentication;
// 使Firebase // Firebase凭证
final UserCredential userCredential = final OAuthCredential credential = GoogleAuthProvider.credential(
await FirebaseAuth.instance.signInWithCredential(credential); accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
return userCredential.user; );
} catch (e) {
print('Google sign-in error: $e'); // 使Firebase
rethrow; // final UserCredential userCredential = await FirebaseAuth.instance
} .signInWithCredential(credential);
}
return userCredential.user;
// } catch (e) {
static Future<User?> signInSilently() async { debugPrint('Google sign-in error: $e');
try { rethrow; //
// }
final GoogleSignInAccount? googleAccount = }
await _g.signInSilently();
//
if (googleAccount == null) return null; static Future<User?> signInSilently() async {
try {
// await initialize();
final GoogleSignInAuthentication googleAuth = //
await googleAccount.authentication; final GoogleSignInAccount? googleAccount = await _g.signInSilently();
// Firebase凭证 if (googleAccount == null) return null;
final OAuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken, //
idToken: googleAuth.idToken, final GoogleSignInAuthentication googleAuth =
); await googleAccount.authentication;
// 使Firebase // Firebase凭证
final UserCredential userCredential = final OAuthCredential credential = GoogleAuthProvider.credential(
await FirebaseAuth.instance.signInWithCredential(credential); accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
return userCredential.user; );
} catch (e) {
print('Silent sign-in error: $e'); // 使Firebase
return null; final UserCredential userCredential = await FirebaseAuth.instance
} .signInWithCredential(credential);
}
return userCredential.user;
// } catch (e) {
static Future<void> signOut() async { debugPrint('Silent sign-in error: $e');
try { return null;
await _g.signOut(); }
await FirebaseAuth.instance.signOut(); }
} catch (e) {
print('Sign out error: $e'); //
rethrow; static Future<void> signOut() async {
} try {
} await initialize();
await _g.signOut();
// await FirebaseAuth.instance.signOut();
static Map<String, dynamic>? getUserInfo() { } catch (e) {
final user = currentUser; debugPrint('Sign out error: $e');
if (user == null) return null; rethrow;
}
return { }
'uid': user.uid,
'name': user.displayName, //
'email': user.email, static Map<String, dynamic>? getUserInfo() {
'photoUrl': user.photoURL, final user = currentUser;
'isEmailVerified': user.emailVerified, if (user == null) return null;
};
} return {
} 'uid': user.uid,
'name': user.displayName,
'email': user.email,
'photoUrl': user.photoURL,
'isEmailVerified': user.emailVerified,
};
}
}

View File

@ -1,53 +1,48 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';
import 'package:flutter/material.dart'; import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
class CustomCachedImage extends StatelessWidget { class CustomCachedImage extends StatelessWidget {
final String imageUrl; final String imageUrl;
final double? width; final double? width;
final double? height; final double? height;
final BoxFit fit; final BoxFit fit;
final double borderRadius; final double borderRadius;
final Widget? placeholder; final Widget? placeholder;
final Widget? errorWidget; final Widget? errorWidget;
const CustomCachedImage({ const CustomCachedImage({
super.key, super.key,
required this.imageUrl, required this.imageUrl,
this.width, this.width,
this.height, this.height,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.borderRadius = 0, this.borderRadius = 0,
this.placeholder, this.placeholder,
this.errorWidget, this.errorWidget,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(borderRadius),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: imageUrl, imageUrl: imageUrl,
width: width, width: width,
height: height, height: height,
fit: fit, fit: fit,
placeholder: (context, url) => placeholder: (context, url) => placeholder ?? _defaultPlaceholder(),
placeholder ?? _defaultPlaceholder(), errorWidget:
errorWidget: (context, url, error) => (context, url, error) => errorWidget ?? _defaultErrorWidget(),
errorWidget ?? _defaultErrorWidget(), ),
), );
); }
}
Widget _defaultPlaceholder() {
Widget _defaultPlaceholder() { return const SCRotatingDotsLoading();
return Center( }
child: CupertinoActivityIndicator(),
); Widget _defaultErrorWidget() {
} return Center(child: Image.asset("sc_images/general/sc_icon_loading.png"));
}
Widget _defaultErrorWidget() { }
return Center(
child: Image.asset("sc_images/general/sc_icon_loading.png"),
);
}
}

View File

@ -12,6 +12,7 @@ import 'package:yumi/app/constants/sc_screen.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_profile_cache.dart'; import 'package:yumi/shared/tools/sc_room_profile_cache.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/sc_rotating_dots_loading.dart';
import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
@ -215,12 +216,7 @@ Widget netImage({
if (loadingWidget != null) { if (loadingWidget != null) {
return loadingWidget; return loadingWidget;
} }
return noDefaultImg return noDefaultImg ? Container() : const SCRotatingDotsLoading();
? Container()
: Image.asset(
defaultImg ?? "sc_images/general/sc_icon_loading.png",
fit: BoxFit.cover,
);
} }
}, },
); );
@ -354,7 +350,7 @@ searchWidget({
height: width(36), height: width(36),
//width: width(345), //width: width(345),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xff18F2B1).withOpacity(0.1), color: const Color(0xff18F2B1).withValues(alpha: 0.1),
borderRadius: BorderRadius.all(Radius.circular(height(20))), borderRadius: BorderRadius.all(Radius.circular(height(20))),
border: Border.all(color: borderColor ?? Colors.black12, width: 1.w), border: Border.all(color: borderColor ?? Colors.black12, width: 1.w),
), ),

View File

@ -0,0 +1,128 @@
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/material.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
class SCRotatingDotsLoading extends StatefulWidget {
const SCRotatingDotsLoading({
super.key,
this.size,
this.color = SocialChatTheme.primaryLight,
this.dotCount = 8,
this.duration = const Duration(milliseconds: 900),
this.minAdaptiveSize = 14,
this.maxAdaptiveSize = 40,
});
final double? size;
final Color color;
final int dotCount;
final Duration duration;
final double minAdaptiveSize;
final double maxAdaptiveSize;
@override
State<SCRotatingDotsLoading> createState() => _SCRotatingDotsLoadingState();
}
class _SCRotatingDotsLoadingState extends State<SCRotatingDotsLoading>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration)
..repeat();
}
@override
void didUpdateWidget(covariant SCRotatingDotsLoading oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.duration != widget.duration) {
_controller
..duration = widget.duration
..repeat();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final configuredSize = widget.size;
if (configuredSize != null) {
return _buildIndicator(configuredSize);
}
return LayoutBuilder(
builder: (context, constraints) {
final adaptiveSize = _resolveAdaptiveSize(constraints);
return Center(child: _buildIndicator(adaptiveSize));
},
);
}
Widget _buildIndicator(double size) {
final normalizedSize = size.clamp(8.0, 200.0);
final dotCount = math.max(1, widget.dotCount);
final maxDotSize = normalizedSize * 0.26;
final radius = normalizedSize / 2 - maxDotSize / 2;
return RepaintBoundary(
child: SizedBox.square(
dimension: normalizedSize,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * math.pi * 2,
child: Stack(
children: List.generate(dotCount, (index) {
final progress = 1 - (index / dotCount);
final dotScale = lerpDouble(0.42, 1, progress)!;
final opacity = lerpDouble(0.18, 1, progress)!;
final dotSize = maxDotSize * dotScale;
final angle = (math.pi * 2 * index / dotCount) - math.pi / 2;
final dx = normalizedSize / 2 + math.cos(angle) * radius;
final dy = normalizedSize / 2 + math.sin(angle) * radius;
return Positioned(
left: dx - dotSize / 2,
top: dy - dotSize / 2,
child: Container(
width: dotSize,
height: dotSize,
decoration: BoxDecoration(
color: widget.color.withValues(alpha: opacity),
shape: BoxShape.circle,
),
),
);
}),
),
);
},
),
),
);
}
double _resolveAdaptiveSize(BoxConstraints constraints) {
final values =
<double>[
constraints.maxWidth,
constraints.maxHeight,
].where((value) => value.isFinite && value > 0).toList();
final shortest = values.isEmpty ? 28.0 : values.reduce(math.min);
final scaled = shortest * 0.52;
return math.max(
widget.minAdaptiveSize,
math.min(widget.maxAdaptiveSize, scaled),
);
}
}

View File

@ -3,6 +3,15 @@
## 当前总目标 ## 当前总目标
- 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 - 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
## 本轮启动优化(非网络)
- 已移除启动页固定 `6` 秒倒计时,改为最短约 `900ms` 展示后继续路由判断,避免人为等待直接拉长入 app 时间。
- 已将 Firebase / Crashlytics 从 `runApp` 前阻塞初始化改为首帧后后台预热,并补充“未就绪时仅本地打印”的异常兜底,减少首屏前平台初始化阻塞。
- 已将 `SocialChatAuthenticationManager``SocialChatUserProfileManager` 改为按需创建,避免 splash 阶段就提前触发 Google Sign-In / Firebase 会话检查。
- 已将 deep link、设备信息、缓存目录创建等非首帧必需动作延后到首帧后执行降低首屏竞争。
- 已优化本地语言初始化:启动时不再重复写入 `SharedPreferences` 并触发一次无意义的 `notifyListeners`
- 已优化首页容器与 Home 首屏:去掉首次进入时“先 loading 再建 tab”的本地跳变并将首页 page 列表、底部导航项从 `build` 阶段的重复重建改为复用。
- 本轮按需求暂未处理网络链路上的启动等待,例如审核态检查或远端启动页配置请求。
## 已完成模块 ## 已完成模块
- 已为语言房页面补充右下方 `game` 悬浮入口:入口位于聊天区右下侧、距右侧约 `15.w`、位于底部操作栏上方约 `100.w`,当前先使用代码绘制的 `🎮` 占位图标,并已在实现处标注后续替换为正式 UI 图片资源。 - 已为语言房页面补充右下方 `game` 悬浮入口:入口位于聊天区右下侧、距右侧约 `15.w`、位于底部操作栏上方约 `100.w`,当前先使用代码绘制的 `🎮` 占位图标,并已在实现处标注后续替换为正式 UI 图片资源。
- 已新增语言房游戏底部弹窗:点击 `game` 入口后会从底部弹出约三分之一屏高的面板,采用房间页现有半透明磨砂风格,便于后续继续沿用当前视觉体系。 - 已新增语言房游戏底部弹窗:点击 `game` 入口后会从底部弹出约三分之一屏高的面板,采用房间页现有半透明磨砂风格,便于后续继续沿用当前视觉体系。
@ -25,6 +34,7 @@
- 已为首页 Party 页补齐首屏骨架屏:顶部 H5 榜单现按三列奖杯卡位展示头像/标题骨架,下方房间区改为双列房卡骨架,并采用深绿底 + 金色描边的项目内风格;同时拆分榜单与房间列表加载态,仅在“首屏无内容加载”时展示骨架,避免下拉刷新时整页闪烁。 - 已为首页 Party 页补齐首屏骨架屏:顶部 H5 榜单现按三列奖杯卡位展示头像/标题骨架,下方房间区改为双列房卡骨架,并采用深绿底 + 金色描边的项目内风格;同时拆分榜单与房间列表加载态,仅在“首屏无内容加载”时展示骨架,避免下拉刷新时整页闪烁。
- 已为首页 My 板块补齐首屏骨架屏:`My room` 顶部房间卡片改为独立加载态,避免首屏把“正在加载我的房间”误显示成“去创建房间”;`Recent/Followed` 两个子页也已改成同风格双列房卡骨架,进入 tab 时会先展示完整结构占位,再按真实数据切换。 - 已为首页 My 板块补齐首屏骨架屏:`My room` 顶部房间卡片改为独立加载态,避免首屏把“正在加载我的房间”误显示成“去创建房间”;`Recent/Followed` 两个子页也已改成同风格双列房卡骨架,进入 tab 时会先展示完整结构占位,再按真实数据切换。
- 已为 `Me` 入口下的 `Store / Bag` 页面补齐首屏骨架屏:四类 `store` 商品页和四类 `bag` 物品页在“首屏空数据加载”阶段都会先展示同风格的三列卡片骨架,不再只显示小菊花;同时已为商品/物品名称补齐宽度约束与省略号处理,避免超长文案顶出卡片。 - 已为 `Me` 入口下的 `Store / Bag` 页面补齐首屏骨架屏:四类 `store` 商品页和四类 `bag` 物品页在“首屏空数据加载”阶段都会先展示同风格的三列卡片骨架,不再只显示小菊花;同时已为商品/物品名称补齐宽度约束与省略号处理,避免超长文案顶出卡片。
- 已将通用图片加载占位从静态 `loading` 图片升级为代码绘制的环形转点组件:礼物二级页单卡封面、`Store / Bag` 商品图以及复用共享图片组件的网络图,在“加载中”阶段都会显示动态转圈效果;而图片失败或 `no data` 时仍保持原有占位图,不混淆加载态与空态。
- 已继续细化首页房卡体验:`My` 板块房卡骨架数量已从 `4` 个补齐到 `6` 个,与首页 Party 首屏保持一致;同时将房卡右下角原本的静态绿色“直播中”图片替换为代码驱动的三段式动态音频条,首页、我的、搜索结果和房间浮窗现都会展示跳动效果,不再依赖静态资源图;最新已把动效调整为“单周期完整峰谷 + A/B/C 错峰起伏”的连续波形,观感更接近直播音频电平。 - 已继续细化首页房卡体验:`My` 板块房卡骨架数量已从 `4` 个补齐到 `6` 个,与首页 Party 首屏保持一致;同时将房卡右下角原本的静态绿色“直播中”图片替换为代码驱动的三段式动态音频条,首页、我的、搜索结果和房间浮窗现都会展示跳动效果,不再依赖静态资源图;最新已把动效调整为“单周期完整峰谷 + A/B/C 错峰起伏”的连续波形,观感更接近直播音频电平。
- 已继续微调首页房卡信息密度:房卡底部房名与在线人数字号已统一调小,和动态音频条更协调;同时已移除 `My` 板块顶部 `my room` 标题及其占位,让房间卡片整体上移,对齐更紧凑。 - 已继续微调首页房卡信息密度:房卡底部房名与在线人数字号已统一调小,和动态音频条更协调;同时已移除 `My` 板块顶部 `my room` 标题及其占位,让房间卡片整体上移,对齐更紧凑。
- 已继续微调首页视觉细节房卡底栏房名再次缩小并轻微上移和国旗、动态音频条、人数的中线更一致首页顶部排行骨架也已简化为“3 个圆形头像 + 1 根文本条”,更贴近真实卡片结构。 - 已继续微调首页视觉细节房卡底栏房名再次缩小并轻微上移和国旗、动态音频条、人数的中线更一致首页顶部排行骨架也已简化为“3 个圆形头像 + 1 根文本条”,更贴近真实卡片结构。
@ -165,6 +175,7 @@
- 首页 Party 页当前已接入贴合真实版式的骨架屏:进入首页首屏时会先看到三张排行卡位与双列房卡占位,骨架配色已跟随页面深绿+鎏金主视觉,不再只显示居中的白色 `loading`;且排行榜与房间区会按各自请求结果独立切换,先返回的区域会先显示真实内容。 - 首页 Party 页当前已接入贴合真实版式的骨架屏:进入首页首屏时会先看到三张排行卡位与双列房卡占位,骨架配色已跟随页面深绿+鎏金主视觉,不再只显示居中的白色 `loading`;且排行榜与房间区会按各自请求结果独立切换,先返回的区域会先显示真实内容。
- 首页 My 板块当前已接入贴合真实版式的骨架屏:顶部 `My room` 现区分“首次加载中”和“确实未创建房间”,不会再先闪出创建房间卡;`Recent/Followed` 在首屏空数据加载时也会先展示双列房卡骨架,不再只显示小菊花或局部空白。 - 首页 My 板块当前已接入贴合真实版式的骨架屏:顶部 `My room` 现区分“首次加载中”和“确实未创建房间”,不会再先闪出创建房间卡;`Recent/Followed` 在首屏空数据加载时也会先展示双列房卡骨架,不再只显示小菊花或局部空白。
- `Me` 入口下的 `Store / Bag` 页面当前也已接入深绿主题的三列卡片骨架:首次进入并且列表尚未返回时,会先显示与正式卡片比例一致的占位;接口返回后再切到真实商品/物品卡片。`store``bag` 卡片标题现都带宽度约束和单行省略,类似截图里的长名称不会再横向撑破布局。 - `Me` 入口下的 `Store / Bag` 页面当前也已接入深绿主题的三列卡片骨架:首次进入并且列表尚未返回时,会先显示与正式卡片比例一致的占位;接口返回后再切到真实商品/物品卡片。`store``bag` 卡片标题现都带宽度约束和单行省略,类似截图里的长名称不会再横向撑破布局。
- 图片加载态与空态当前已拆分:共享网络图组件和礼物二级页单卡封面加载时会显示代码绘制的环形转点,不再继续复用 `sc_icon_loading.png`;若图片失败或确实无数据,仍旧保留原先那张占位图,避免把 `loading``no data` 看成同一种状态。
- 首页与搜索等房卡右下角的在线状态图标当前已改为代码绘制的动态音频条:三根绿条会按不同相位走完整的波峰波谷周期,错峰连续起伏,视觉上更接近直播/语聊房的实时状态;`My` 板块骨架数量也已与 Party 页对齐为 `6` 个。 - 首页与搜索等房卡右下角的在线状态图标当前已改为代码绘制的动态音频条:三根绿条会按不同相位走完整的波峰波谷周期,错峰连续起伏,视觉上更接近直播/语聊房的实时状态;`My` 板块骨架数量也已与 Party 页对齐为 `6` 个。
- 首页房卡底部信息当前已进一步收紧:房名和人数文字比上一版更小,底栏与动态音频条的视觉重心更一致;`My` 页顶部 `my room` 标题也已移除,下方卡片和 tab 会自然上移填充。 - 首页房卡底部信息当前已进一步收紧:房名和人数文字比上一版更小,底栏与动态音频条的视觉重心更一致;`My` 页顶部 `my room` 标题也已移除,下方卡片和 tab 会自然上移填充。
- 首页顶部排行骨架当前已进一步收窄为单条标题占位,不再保留两条文本骨架;房卡底栏房名也已再次缩一号并做轻微上移,对齐更贴近设计稿观感。 - 首页顶部排行骨架当前已进一步收窄为单条标题占位,不再保留两条文本骨架;房卡底栏房名也已再次缩一号并做轻微上移,对齐更贴近设计稿观感。