547 lines
17 KiB
Dart
547 lines
17 KiB
Dart
import 'dart:async';
|
||
import 'dart:math';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/shared/tools/sc_version_utils.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||
import 'package:yumi/app/routes/sc_routes.dart';
|
||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||
import 'package:yumi/modules/auth/login_route.dart';
|
||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||
|
||
import 'last_weekly_cp_splash_cache.dart';
|
||
import 'weekly_star_splash_cache.dart';
|
||
|
||
enum _SplashPresentationVariant { weeklyStar, lastWeeklyCp, defaultSplash }
|
||
|
||
/// CP 启动页手动微调区。
|
||
/// 如果后续还要继续对齐设计稿,优先只改这一组 Rect / Size,
|
||
/// 不要去动 _buildCpRankCard 里的层级和结构。
|
||
class _LastWeeklyCpLayout {
|
||
static const Rect rank1FrameRect = Rect.fromLTWH(40, 253, 297, 148);
|
||
static const Rect rank2FrameRect = Rect.fromLTWH(0, 427, 196, 111);
|
||
static const Rect rank3FrameRect = Rect.fromLTWH(179, 427, 196, 111);
|
||
|
||
static final Rect rank1LeftAvatarRect = Rect.fromCenter(
|
||
center: const Offset(151, 353),
|
||
width: 58,
|
||
height: 58,
|
||
);
|
||
static final Rect rank1RightAvatarRect = Rect.fromCenter(
|
||
center: const Offset(227, 352),
|
||
width: 58,
|
||
height: 58,
|
||
);
|
||
static const Rect rank1LabelRect = Rect.fromLTWH(118, 383, 140, 22);
|
||
|
||
static final Rect rank2LeftAvatarRect = Rect.fromCenter(
|
||
center: const Offset(46, 508),
|
||
width: 50,
|
||
height: 50,
|
||
);
|
||
static final Rect rank2RightAvatarRect = Rect.fromCenter(
|
||
center: const Offset(123, 509),
|
||
width: 50,
|
||
height: 50,
|
||
);
|
||
static const Rect rank2LabelRect = Rect.fromLTWH(10, 533, 145, 26);
|
||
|
||
static final Rect rank3LeftAvatarRect = Rect.fromCenter(
|
||
center: const Offset(252, 508),
|
||
width: 50,
|
||
height: 50,
|
||
);
|
||
static final Rect rank3RightAvatarRect = Rect.fromCenter(
|
||
center: const Offset(329, 509),
|
||
width: 50,
|
||
height: 50,
|
||
);
|
||
static const Rect rank3LabelRect = Rect.fromLTWH(218, 533, 145, 26);
|
||
}
|
||
|
||
class SplashPage extends StatefulWidget {
|
||
const SplashPage({super.key});
|
||
|
||
@override
|
||
State<SplashPage> createState() => _SplashPageState();
|
||
}
|
||
|
||
class _SplashPageState extends State<SplashPage> {
|
||
static const Duration _minimumSplashDuration = Duration(seconds: 3);
|
||
static const Size _weeklyStarCanvasSize = Size(375, 812);
|
||
static const Size _lastWeeklyCpCanvasSize = Size(375, 812);
|
||
|
||
Timer? _timer;
|
||
Timer? _countdownTimer;
|
||
late final List<WeeklyStarSplashEntry> _weeklyStarEntries;
|
||
late final List<LastWeeklyCPSplashEntry> _lastWeeklyCpEntries;
|
||
late final _SplashPresentationVariant _selectedVariant;
|
||
int _remainingSeconds = _minimumSplashDuration.inSeconds;
|
||
bool _isNavigating = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_weeklyStarEntries = WeeklyStarSplashCache.loadDisplayEntries();
|
||
_lastWeeklyCpEntries = LastWeeklyCPSplashCache.loadDisplayEntries();
|
||
_selectedVariant = _pickSplashVariant();
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
unawaited(FileCacheManager.getInstance().getFilePath());
|
||
unawaited(WeeklyStarSplashCache.refreshCacheInBackground());
|
||
unawaited(LastWeeklyCPSplashCache.refreshCacheInBackground());
|
||
});
|
||
_startNavigationTimer();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_cancelTimer();
|
||
super.dispose();
|
||
}
|
||
|
||
/// 启动页最短展示时间
|
||
void _startNavigationTimer() {
|
||
_remainingSeconds = _minimumSplashDuration.inSeconds;
|
||
_timer = Timer(_minimumSplashDuration, _dismissSplash);
|
||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||
final nextSeconds = _minimumSplashDuration.inSeconds - timer.tick;
|
||
if (nextSeconds <= 0) {
|
||
timer.cancel();
|
||
return;
|
||
}
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
_remainingSeconds = nextSeconds;
|
||
});
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final splashContent = switch (_selectedVariant) {
|
||
_SplashPresentationVariant.weeklyStar => _buildWeeklyStarSplash(),
|
||
_SplashPresentationVariant.lastWeeklyCp => _buildLastWeeklyCpSplash(),
|
||
_SplashPresentationVariant.defaultSplash => _buildDefaultSplash(),
|
||
};
|
||
final showLaunchOverlay =
|
||
_selectedVariant != _SplashPresentationVariant.defaultSplash;
|
||
|
||
return Material(
|
||
color: Colors.black,
|
||
child: Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
splashContent,
|
||
if (showLaunchOverlay) _buildSkipCountdownButton(context),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
_SplashPresentationVariant _pickSplashVariant() {
|
||
final variants = <_SplashPresentationVariant>[];
|
||
if (WeeklyStarSplashCache.shouldShow) {
|
||
variants.add(_SplashPresentationVariant.weeklyStar);
|
||
}
|
||
if (LastWeeklyCPSplashCache.shouldShow) {
|
||
variants.add(_SplashPresentationVariant.lastWeeklyCp);
|
||
}
|
||
if (variants.isEmpty) {
|
||
return _SplashPresentationVariant.defaultSplash;
|
||
}
|
||
return variants[Random().nextInt(variants.length)];
|
||
}
|
||
|
||
Widget _buildDefaultSplash() {
|
||
return Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
Image.asset(
|
||
SCGlobalConfig.businessLogicStrategy.getSplashPageBackgroundImage(),
|
||
width: ScreenUtil().screenWidth,
|
||
height: ScreenUtil().screenHeight,
|
||
fit: BoxFit.cover,
|
||
),
|
||
Image.asset(
|
||
SCGlobalConfig.businessLogicStrategy.getSplashPageIcon(),
|
||
width: 107.w,
|
||
height: 159.w,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildSkipCountdownButton(BuildContext context) {
|
||
final localizations = SCAppLocalizations.of(context);
|
||
final label =
|
||
localizations?.skipCountdown(_remainingSeconds.toString()) ??
|
||
'Skip ${_remainingSeconds}s';
|
||
final borderRadius = BorderRadius.circular(999);
|
||
|
||
return SafeArea(
|
||
child: Align(
|
||
alignment: AlignmentDirectional.topEnd,
|
||
child: Padding(
|
||
padding: EdgeInsetsDirectional.only(top: 12.h, end: 16.w),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: Ink(
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
colors: [Color(0x33FFFFFF), Color(0x18FFFFFF)],
|
||
begin: AlignmentDirectional.centerStart,
|
||
end: AlignmentDirectional.centerEnd,
|
||
),
|
||
borderRadius: borderRadius,
|
||
border: Border.all(color: Colors.white.withValues(alpha: 0.18)),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.1),
|
||
blurRadius: 10.w,
|
||
offset: Offset(0, 3.w),
|
||
),
|
||
],
|
||
),
|
||
child: InkWell(
|
||
borderRadius: borderRadius,
|
||
onTap: _dismissSplash,
|
||
splashFactory: NoSplash.splashFactory,
|
||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||
splashColor: Colors.transparent,
|
||
highlightColor: Colors.transparent,
|
||
hoverColor: Colors.transparent,
|
||
child: Padding(
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: 12.w,
|
||
vertical: 8.h,
|
||
),
|
||
child: Text(
|
||
label,
|
||
maxLines: 1,
|
||
softWrap: false,
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
color: Colors.white.withValues(alpha: 0.96),
|
||
fontSize: 12.sp,
|
||
height: 1,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildWeeklyStarSplash() {
|
||
return SizedBox.expand(
|
||
child: FittedBox(
|
||
fit: BoxFit.cover,
|
||
child: SizedBox(
|
||
width: _weeklyStarCanvasSize.width,
|
||
height: _weeklyStarCanvasSize.height,
|
||
child: Stack(
|
||
children: [
|
||
Positioned.fill(
|
||
child: Image.asset(
|
||
'sc_images/splash/sc_weekly_star_bg.png',
|
||
fit: BoxFit.cover,
|
||
),
|
||
),
|
||
_buildRankCard(
|
||
entry: _entryForRank(1),
|
||
frameAsset: 'sc_images/splash/sc_icon_weekly_star_rank_1.png',
|
||
frameRect: const Rect.fromLTWH(24, 110, 327, 248),
|
||
avatarRect: Rect.fromCenter(
|
||
center: Offset(188, 261),
|
||
width: 74,
|
||
height: 74,
|
||
),
|
||
labelRect: const Rect.fromLTWH(154, 328, 67, 12),
|
||
),
|
||
_buildRankCard(
|
||
entry: _entryForRank(2),
|
||
frameAsset: 'sc_images/splash/sc_icon_weekly_star_rank_2.png',
|
||
frameRect: const Rect.fromLTWH(0, 376, 182, 115),
|
||
avatarRect: Rect.fromCenter(
|
||
center: Offset(77, 437),
|
||
width: 62,
|
||
height: 62,
|
||
),
|
||
labelRect: const Rect.fromLTWH(40, 501, 74, 12),
|
||
),
|
||
_buildRankCard(
|
||
entry: _entryForRank(3),
|
||
frameAsset: 'sc_images/splash/sc_icon_weekly_star_rank_3.png',
|
||
frameRect: const Rect.fromLTWH(180, 376, 195, 117),
|
||
avatarRect: Rect.fromCenter(
|
||
center: Offset(299, 439),
|
||
width: 66,
|
||
height: 66,
|
||
),
|
||
labelRect: const Rect.fromLTWH(260, 501, 78, 12),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildLastWeeklyCpSplash() {
|
||
return SizedBox.expand(
|
||
child: FittedBox(
|
||
fit: BoxFit.cover,
|
||
child: SizedBox(
|
||
width: _lastWeeklyCpCanvasSize.width,
|
||
height: _lastWeeklyCpCanvasSize.height,
|
||
child: Stack(
|
||
children: [
|
||
Positioned.fill(
|
||
child: Image.asset(
|
||
'sc_images/splash/sc_last_weekly_cp_bg.png',
|
||
fit: BoxFit.cover,
|
||
),
|
||
),
|
||
_buildCpRankCard(
|
||
entry: _cpEntryForRank(1),
|
||
frameAsset:
|
||
'sc_images/splash/sc_icon_last_weekly_cp_rank_1.png',
|
||
frameRect: _LastWeeklyCpLayout.rank1FrameRect,
|
||
leftAvatarRect: _LastWeeklyCpLayout.rank1LeftAvatarRect,
|
||
rightAvatarRect: _LastWeeklyCpLayout.rank1RightAvatarRect,
|
||
labelRect: _LastWeeklyCpLayout.rank1LabelRect,
|
||
),
|
||
_buildCpRankCard(
|
||
entry: _cpEntryForRank(2),
|
||
frameAsset:
|
||
'sc_images/splash/sc_icon_last_weekly_cp_rank_2.png',
|
||
frameRect: _LastWeeklyCpLayout.rank2FrameRect,
|
||
leftAvatarRect: _LastWeeklyCpLayout.rank2LeftAvatarRect,
|
||
rightAvatarRect: _LastWeeklyCpLayout.rank2RightAvatarRect,
|
||
labelRect: _LastWeeklyCpLayout.rank2LabelRect,
|
||
),
|
||
_buildCpRankCard(
|
||
entry: _cpEntryForRank(3),
|
||
frameAsset:
|
||
'sc_images/splash/sc_icon_last_weekly_cp_rank_3.png',
|
||
frameRect: _LastWeeklyCpLayout.rank3FrameRect,
|
||
leftAvatarRect: _LastWeeklyCpLayout.rank3LeftAvatarRect,
|
||
rightAvatarRect: _LastWeeklyCpLayout.rank3RightAvatarRect,
|
||
labelRect: _LastWeeklyCpLayout.rank3LabelRect,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
WeeklyStarSplashEntry _entryForRank(int rank) {
|
||
return _weeklyStarEntries.firstWhere(
|
||
(entry) => entry.rank == rank,
|
||
orElse:
|
||
() => WeeklyStarSplashEntry(
|
||
rank: rank,
|
||
avatarUrl: '',
|
||
nickname: 'Namename',
|
||
),
|
||
);
|
||
}
|
||
|
||
LastWeeklyCPSplashEntry _cpEntryForRank(int rank) {
|
||
return _lastWeeklyCpEntries.firstWhere(
|
||
(entry) => entry.rank == rank,
|
||
orElse:
|
||
() => LastWeeklyCPSplashEntry(
|
||
rank: rank,
|
||
leftAvatarUrl: '',
|
||
rightAvatarUrl: '',
|
||
leftNickname: 'Namename',
|
||
rightNickname: 'Namename',
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildRankCard({
|
||
required WeeklyStarSplashEntry entry,
|
||
required String frameAsset,
|
||
required Rect frameRect,
|
||
required Rect avatarRect,
|
||
required Rect labelRect,
|
||
}) {
|
||
return Stack(
|
||
children: [
|
||
Positioned.fromRect(
|
||
rect: avatarRect,
|
||
child: head(url: entry.avatarUrl, width: avatarRect.width),
|
||
),
|
||
Positioned.fromRect(
|
||
rect: frameRect,
|
||
child: Image.asset(frameAsset, fit: BoxFit.fill),
|
||
),
|
||
Positioned.fromRect(
|
||
rect: labelRect,
|
||
child: _SplashOutlinedText(text: entry.nickname),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildCpRankCard({
|
||
required LastWeeklyCPSplashEntry entry,
|
||
required String frameAsset,
|
||
required Rect frameRect,
|
||
required Rect leftAvatarRect,
|
||
required Rect rightAvatarRect,
|
||
required Rect labelRect,
|
||
}) {
|
||
return Stack(
|
||
children: [
|
||
Positioned.fromRect(
|
||
rect: frameRect,
|
||
child: Image.asset(frameAsset, fit: BoxFit.fill),
|
||
),
|
||
Positioned.fromRect(
|
||
rect: leftAvatarRect,
|
||
child: head(url: entry.leftAvatarUrl, width: leftAvatarRect.width),
|
||
),
|
||
Positioned.fromRect(
|
||
rect: rightAvatarRect,
|
||
child: head(url: entry.rightAvatarUrl, width: rightAvatarRect.width),
|
||
),
|
||
Positioned.fromRect(
|
||
rect: labelRect,
|
||
child: _LastWeeklyCpPairLabel(
|
||
leftText: entry.leftNickname,
|
||
rightText: entry.rightNickname,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
void _dismissSplash() {
|
||
if (_isNavigating) {
|
||
return;
|
||
}
|
||
_isNavigating = true;
|
||
_cancelTimer();
|
||
unawaited(_goMainPage());
|
||
}
|
||
|
||
Future<void> _goMainPage() async {
|
||
try {
|
||
await SCVersionUtils.checkReview();
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
var user = AccountStorage().getCurrentUser();
|
||
var token = AccountStorage().getToken();
|
||
if (user != null && token.isNotEmpty) {
|
||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||
} else {
|
||
SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
|
||
}
|
||
} catch (e) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
|
||
}
|
||
}
|
||
|
||
/// 取消倒计时的计时器。
|
||
void _cancelTimer() {
|
||
// 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。
|
||
_timer?.cancel();
|
||
_countdownTimer?.cancel();
|
||
}
|
||
}
|
||
|
||
class _SplashOutlinedText extends StatelessWidget {
|
||
const _SplashOutlinedText({required this.text});
|
||
|
||
final String text;
|
||
|
||
static const Color _fillColor = Color(0xFFA11726);
|
||
static const Color _strokeColor = Color(0xFFFFEBBA);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final displayText = text.trim().isEmpty ? 'Namename' : text.trim();
|
||
return FittedBox(
|
||
fit: BoxFit.scaleDown,
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
Text(
|
||
displayText,
|
||
maxLines: 1,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
height: 1,
|
||
fontWeight: FontWeight.w500,
|
||
foreground:
|
||
Paint()
|
||
..style = PaintingStyle.stroke
|
||
..strokeWidth = 1
|
||
..color = _strokeColor,
|
||
),
|
||
),
|
||
Text(
|
||
displayText,
|
||
maxLines: 1,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
height: 1,
|
||
fontWeight: FontWeight.w500,
|
||
color: _fillColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LastWeeklyCpPairLabel extends StatelessWidget {
|
||
const _LastWeeklyCpPairLabel({
|
||
required this.leftText,
|
||
required this.rightText,
|
||
});
|
||
|
||
final String leftText;
|
||
final String rightText;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Expanded(
|
||
child: Align(
|
||
alignment: Alignment.centerRight,
|
||
child: _SplashOutlinedText(text: leftText),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: _SplashOutlinedText(text: rightText),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|