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) {
|
|
|
|
|
if (!NextStep.I.j) {
|
|
|
|
|
return url;
|
|
|
|
|
}
|
|
|
|
|
if (token.isEmpty) return url;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final uri = Uri.parse(url);
|
|
|
|
|
final params = Map<String, String>.from(uri.queryParameters);
|
|
|
|
|
params['token'] = token;
|
|
|
|
|
return uri.replace(queryParameters: params).toString();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('append web login token failed: $e');
|
|
|
|
|
return url;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
'currentUrl': _safeReportUrl(_currentWebViewUrl),
|
|
|
|
|
'redirectUrl': _safeReportUrl(_redirect),
|
|
|
|
|
'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,
|
2026-06-07 17:53:19 +08:00
|
|
|
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
|
|
|
),
|
2026-06-07 17:53:19 +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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-06-07 17:53:19 +08:00
|
|
|
),
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
// 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(() {
|
2026-06-07 17:53:19 +08:00
|
|
|
_showWebViewProgress(progress: 0);
|
2026-06-02 19:01:08 +08:00
|
|
|
});
|
2026-06-07 17:53:19 +08:00
|
|
|
}
|
|
|
|
|
debugPrint('WebView 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) {
|
2026-06-07 17:53:19 +08:00
|
|
|
_webViewProgress = webViewProgress;
|
|
|
|
|
} else {
|
|
|
|
|
_showWebViewProgress(progress: webViewProgress);
|
2026-06-02 19:01:08 +08:00
|
|
|
}
|
2026-06-07 17:53:19 +08:00
|
|
|
});
|
|
|
|
|
if (progress >= 100) {
|
|
|
|
|
_hideWebViewProgressSoon();
|
|
|
|
|
}
|
|
|
|
|
debugPrint('WebView 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(
|
2026-06-07 17:53:19 +08:00
|
|
|
'WebView skip redirect onLoadStop: $url -> $_pendingRedirectUrl',
|
2026-06-02 19:01:08 +08:00
|
|
|
);
|
2026-06-07 17:53:19 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (mounted && !_webViewFirstLoadDone) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_webViewProgress = 1;
|
|
|
|
|
_pendingRedirectUrl = null;
|
2026-06-02 19:01:08 +08:00
|
|
|
});
|
2026-06-07 17:53:19 +08:00
|
|
|
_hideWebViewProgressSoon();
|
|
|
|
|
}
|
|
|
|
|
_reportWebViewEvent('load_success', {
|
|
|
|
|
'url': _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
|
|
|
),
|
2026-06-07 17:53:19 +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-07 17:53:19 +08:00
|
|
|
),
|
|
|
|
|
],
|
2026-06-02 19:01:08 +08:00
|
|
|
),
|
|
|
|
|
),
|
2026-06-07 17:53:19 +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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|