game_page.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:math' as math;
  5. import 'package:flutter/gestures.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:get_it/get_it.dart';
  9. import 'package:intl/intl.dart';
  10. import 'package:provider/provider.dart';
  11. import 'package:shared_preferences/shared_preferences.dart';
  12. import 'package:sport/application.dart';
  13. import 'package:sport/bean/game.dart';
  14. import 'package:sport/bean/game_add_entity.dart';
  15. import 'package:sport/db/statistics_db.dart';
  16. import 'package:sport/game/sdk_parse.dart';
  17. import 'package:sport/pages/web/game_log.dart';
  18. import 'package:sport/pages/web/statistics.dart';
  19. import 'package:sport/provider/bluetooth.dart';
  20. import 'package:sport/provider/user_model.dart';
  21. import 'package:sport/services/Converter.dart';
  22. import 'package:sport/services/api/inject_api.dart';
  23. import 'package:sport/services/api/resp.dart';
  24. import 'package:sport/services/app_subscription_state.dart';
  25. import 'package:sport/utils/task_queue.dart';
  26. import 'package:sport/utils/toast.dart';
  27. import 'package:sport/widgets/dialog/alert_dialog.dart';
  28. import 'package:sport/widgets/dialog/ble_wait_dialog.dart';
  29. import 'package:sport/widgets/dialog/request_dialog.dart';
  30. import 'package:wakelock/wakelock.dart';
  31. import 'package:web_socket_channel/io.dart';
  32. import 'package:webview_flutter/webview_flutter.dart';
  33. class GamePage extends StatefulWidget {
  34. final GameInfoData game;
  35. final bool gameCenter;
  36. const GamePage({Key? key, required this.game, this.gameCenter = false}) : super(key: key);
  37. @override
  38. State<StatefulWidget> createState() => _PageState();
  39. }
  40. class _PageState extends State<GamePage> with SubscriptionState, InjectApi {
  41. WebViewController? _controller;
  42. GlobalKey _key = GlobalKey();
  43. late Set<JavascriptChannel> javascriptChannels;
  44. ValueNotifier<int> progressNotifier = ValueNotifier<int>(0);
  45. late Bluetooth bluetooth;
  46. Timer? _timer;
  47. Timer? _timerGame;
  48. bool _onPageFinished = false;
  49. bool _onSdkLoaded = false;
  50. bool _gamePlaying = false;
  51. bool _gameFinished = false;
  52. bool _openGameData = false;
  53. bool _openGameAtt = false;
  54. bool _vibrate = false;
  55. Statistics? _statistics;
  56. late String playGroup;
  57. late StatisticsDB _db;
  58. late SDKApi sdk;
  59. late int sdkVersion = 0;
  60. @override
  61. void initState() {
  62. super.initState();
  63. javascriptChannels = Set.of([JavascriptChannel(name: "SDKBridge", onMessageReceived: _onMessageReceived)]);
  64. playGroup = "${DateTime.now().millisecondsSinceEpoch}";
  65. bluetooth = GetIt.I();
  66. sdk = bluetooth.gameSDK();
  67. _initGame();
  68. _timer = Timer.periodic(Duration(milliseconds: 16), (timer) {
  69. if (_onPageFinished != true) return;
  70. if (appLifecycleState != AppLifecycleState.resumed) return;
  71. if (_gamePlaying == true) {
  72. int stepFreq = sdk.getStepFreq();
  73. int stepCount = sdk.getStepCount();
  74. // if (_stepCount == stepCount) return;
  75. if (sdkVersion >= 1) {
  76. int velocity = sdk.getGameStepVel();
  77. if (isDebugShoe) print("sdk -- step $stepFreq, $stepCount, $velocity");
  78. _js("stepData", "$stepFreq,$stepCount,$velocity");
  79. } else {
  80. if (isDebugShoe) print("sdk -- step $stepFreq, $stepCount");
  81. _js("step", "$stepFreq,$stepCount");
  82. }
  83. }
  84. if(_openGameAtt){
  85. if (isDebugShoe) print("sdk -- att ${sdk.getAttX()} ${-(sdk.getAttX() / 10000.0 * 57.29578)}");
  86. _js("att", "${sdk.getAttX()},0,0");
  87. }
  88. });
  89. _timerGame = Timer.periodic(Duration(seconds: 5), (timer) {
  90. if (_gamePlaying == true) _updateGameState();
  91. if(!bluetooth.isConnected){
  92. showWaitDialog(context);
  93. return ;
  94. }
  95. if(bluetooth.h5gameRNotifier.value != true){
  96. bluetooth.setupGameMode4h5(true);
  97. showWaitDialog(context);
  98. }
  99. });
  100. addSubscription(bluetooth.sdkCmdStream.listen((event) {
  101. int cmd = event;
  102. _js("cmd", "0,$cmd");
  103. if (cmd == 6) {
  104. bluetooth.vibrate(200, leftOrRight: 1);
  105. }
  106. if (cmd == 5) {
  107. bluetooth.vibrate(200, leftOrRight: 2);
  108. }
  109. }));
  110. addSubscription(bluetooth.sdkMotionDataStream.listen((event) {
  111. String gameData = event;
  112. if (gameData.length > 10) {
  113. if (_openGameData == true) _js("gameData", "[$gameData]");
  114. if (isDebugShoe) {
  115. channel?.sink.add("$gameData,0");
  116. }
  117. }
  118. }));
  119. addSubscription(bluetooth.sdkMotionStream.listen((event) {
  120. if (_onPageFinished != true) return;
  121. // if (_gamePlaying == true) {
  122. List<int> result = event;
  123. if(result.take(4).any((element) => element > 0)) {
  124. _js("motion", "0,0,${result[0]},${result[1]},${result[2]},${result[3]}");
  125. }
  126. // }
  127. }));
  128. Wakelock.enable();
  129. bluetooth.uploadGameLog();
  130. if (isDebugShoe) {
  131. try {
  132. channel = IOWebSocketChannel.connect(Uri.parse('ws://172.16.14.127/examples/websocket/chat'));
  133. } catch (e) {
  134. print(e);
  135. }
  136. }
  137. _db = StatisticsDB();
  138. SharedPreferences.getInstance().then((prefs) {
  139. this._vibrate = prefs.getBool("vibrate") ?? true;
  140. });
  141. WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
  142. if ((widget.game.h5 ?? 0) == 1) {
  143. SystemChrome.setPreferredOrientations(Platform.isIOS ? [DeviceOrientation.landscapeRight] : [DeviceOrientation.landscapeLeft]);
  144. }
  145. SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
  146. });
  147. }
  148. @override
  149. void dispose() {
  150. progressNotifier.dispose();
  151. _gameFinished = true;
  152. Wakelock.disable();
  153. bluetooth.setupGameMode4h5(widget.gameCenter);
  154. if (!widget.gameCenter) {
  155. if ((widget.game.h5 ?? 0) == 1) {
  156. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  157. }
  158. SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]);
  159. }
  160. _controller = null;
  161. _onGameEnd(isCancel: 1, forceExit: true);
  162. _timer?.cancel();
  163. _timerGame?.cancel();
  164. super.dispose();
  165. channel?.sink.close();
  166. }
  167. _js(String call, String args) {
  168. var script = "if(window.SHOES_SDK != undefined && window.SHOES_SDK != null && window.SHOES_SDK['$call'] != undefined){window.SHOES_SDK.$call($args);}";
  169. // print("sdk -- call $script");
  170. if (appLifecycleState == AppLifecycleState.resumed) _controller?.runJavascript(script);
  171. }
  172. _updateGameState({int level = 0, double score = 0, int record = 0, int mode = 0, int opponentId = 0, int isCancel = 0}) {
  173. if (_statistics != null) {
  174. for (var i = 0; i < MOTION_COUNT_TYPE.values.length; i++) {
  175. _statistics!.data[i] = sdk.getMotionCount(i);
  176. }
  177. _statistics!.data[(MOTION_COUNT_TYPE.JUMP_COUNT.index)] += _statistics!.data[(MOTION_COUNT_TYPE.ROCK_COUNT.index)] + _statistics!.data[(MOTION_COUNT_TYPE.SCISSORS_COUNT.index)] + _statistics!.data[(MOTION_COUNT_TYPE.PAPER_COUNT.index)];
  178. _statistics!.end = DateTime.now().millisecondsSinceEpoch;
  179. if (level > 0) _statistics!.level = level;
  180. if (score > 0) _statistics!.score = score;
  181. if (record > 0) _statistics!.record = record;
  182. if (mode > 0) _statistics!.mode = mode;
  183. if (opponentId > 0) _statistics!.opponentId = opponentId;
  184. if (isCancel > 0) _statistics!.status = isCancel;
  185. _db.update(_statistics!);
  186. }
  187. }
  188. _initGame() {
  189. bluetooth.setupGameMode4h5(true);
  190. sdk.gameInit(widget.game.gameType ?? 0);
  191. }
  192. _onGameStart() {
  193. _initGame();
  194. _gamePlaying = true;
  195. _statistics = Statistics.name("${widget.game.id}", DateTime.now().millisecondsSinceEpoch);
  196. _db.save(_statistics!).then((value) => _statistics!.id = value);
  197. if (isDebugShoe) {
  198. bluetooth.createGameLog("${widget.game.name}");
  199. channel?.sink.add("start");
  200. }
  201. }
  202. _onGameEnd({int level = 0, double score = 0, int record = 0, int mode = 0, int opponentId = 0, int isCancel = 0, bool forceExit = false}) async {
  203. _gamePlaying = false;
  204. if (_statistics == null) return;
  205. _updateGameState(level: level, score: score, record: record, mode: mode, opponentId: opponentId, isCancel: isCancel);
  206. int jump = _statistics!.data[(MOTION_COUNT_TYPE.JUMP_COUNT.index)] + _statistics!.data[(MOTION_COUNT_TYPE.ROCK_COUNT.index)] + _statistics!.data[(MOTION_COUNT_TYPE.SCISSORS_COUNT.index)] + _statistics!.data[(MOTION_COUNT_TYPE.PAPER_COUNT.index)];
  207. int down = _statistics!.data[(MOTION_COUNT_TYPE.DOWN_COUNT.index)];
  208. int step = _statistics!.data[(MOTION_COUNT_TYPE.STEP_COUNT.index)];
  209. //
  210. // int consume = widget.game.totalConsume(step, jump, down);
  211. // int equivalent = (SportUtils.consumeToMinute(consume, 4.3, weight) * 60 * 0.7).round();
  212. print("sdk -- data ${_statistics?.data}");
  213. int duration = (_statistics!.end - _statistics!.start) ~/ 1000;
  214. int id = _statistics?.id ?? 0;
  215. if (duration < 1) {
  216. _db.deleteRecord(id);
  217. return;
  218. }
  219. String movements = _statistics?.movements ?? "";
  220. int start = _statistics!.start;
  221. _statistics = null;
  222. ApiCall upload = () async {
  223. final completer = Completer<RespData<GameAddEntity>>();
  224. Timer timer = Timer(Duration(seconds: 5), () {
  225. if (!completer.isCompleted) completer.completeError("timeout");
  226. });
  227. var queue = TaskQueue();
  228. var listen = queue.stream.listen((event) {
  229. if (event.id == id && event.type == TaskType.GAME.index && event.state == 0) {
  230. print("sdk -- post succ ${event.id}");
  231. _db.deleteRecord(id);
  232. if (!completer.isCompleted) completer.complete(event.result as RespData<GameAddEntity>);
  233. }
  234. });
  235. var time = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(start));
  236. queue.addTask(id, TaskType.GAME, () => api.postAddGame("${widget.game.id}", score, record, level, mode, duration, 0, 0, jump, down, step, 0, opponentId, isCancel, playGroup, movements, time));
  237. try {
  238. RespData<GameAddEntity> resp = await completer.future;
  239. timer.cancel();
  240. if (resp.code == 0 && resp.data?.record != null) {
  241. _db.deleteRecord(id);
  242. GameAddRecord? record = resp.data?.record;
  243. return "{'consume':${record?.consume},'equivalent':${record?.equivalent},'unit':'${record?.unit}','position':${resp.data?.rankResult.isNotEmpty == true ? resp.data?.rankResult.first.position : 0}}";
  244. }
  245. } catch (e) {
  246. print(e);
  247. }
  248. listen.cancel();
  249. return "{'consume':0,'equivalent':0,'unit':'步','position':0}";
  250. };
  251. if (forceExit) {
  252. upload.call();
  253. } else {
  254. request(context, upload).then((value) => _js("callback", "'onGameEnd_callback', $value"));
  255. }
  256. bluetooth.uploadGameLog();
  257. }
  258. IOWebSocketChannel? channel;
  259. _onMessageReceived(JavascriptMessage message) async {
  260. String msg = message.message;
  261. var data = json.decode(msg);
  262. String? method = data['method'];
  263. debugPrint("sdk -- call $method");
  264. if (method == null) return;
  265. if (_onSdkLoaded != true) {
  266. setState(() {
  267. _onSdkLoaded = true;
  268. });
  269. }
  270. if ("onLoad" == method) {
  271. var args = data['args'];
  272. sdkVersion = (args['ver'] as int?) ?? 0;
  273. // if (Platform.isIOS) {
  274. var mediaQuery = MediaQuery.of(context);
  275. var offset = Offset(mediaQuery.size.width / 2, mediaQuery.viewPadding.top + 5);
  276. GestureBinding.instance!.handlePointerEvent(PointerAddedEvent(pointer: 0, position: offset));
  277. GestureBinding.instance!.handlePointerEvent(PointerDownEvent(pointer: 0, position: offset));
  278. GestureBinding.instance!.handlePointerEvent(PointerUpEvent(pointer: 0, position: offset));
  279. // }
  280. } else if ("onBackPressed" == method) {
  281. var args = data['args'];
  282. var foot = (args['foot'] as bool?) ?? false;
  283. _controller?.canGoBack().then((value) {
  284. if (value) {
  285. _controller?.goBack();
  286. } else {
  287. setState(() {
  288. Navigator.pop(context, foot);
  289. });
  290. }
  291. });
  292. } else if ("onGameStart" == method) {
  293. _onGameStart();
  294. } else if ("onGameEnd" == method) {
  295. var args = data['args'];
  296. var level = Converter.toInt(args['level']);
  297. var score = Converter.toDouble(args['score']);
  298. var record = Converter.toInt(args['record']);
  299. var mode = Converter.toInt(args['mode']);
  300. var opponentId = Converter.toInt(args['opponentId']);
  301. _onGameEnd(level: level, score: score, record: record, mode: mode, opponentId: opponentId);
  302. } else if ("getUserInfo_callback" == method) {
  303. var user = Provider.of<UserModel>(context, listen: false).user;
  304. _js("callback", "'getUserInfo_callback', ${json.encode(user.toJsonSimple())}");
  305. if (Platform.isIOS) {
  306. var mediaQuery = MediaQuery.of(context);
  307. var offset = Offset(mediaQuery.size.width / 2, mediaQuery.viewPadding.top + 5);
  308. GestureBinding.instance!.handlePointerEvent(PointerAddedEvent(pointer: 0, position: offset));
  309. GestureBinding.instance!.handlePointerEvent(PointerDownEvent(pointer: 0, position: offset));
  310. GestureBinding.instance!.handlePointerEvent(PointerUpEvent(pointer: 0, position: offset));
  311. }
  312. } else if ("getRank_callback" == method) {
  313. var args = data['args'];
  314. var type = Converter.toInt(args['type']);
  315. var resp = await api.getRankInfo("${widget.game.id}", scope: type == 0 ? "world" : "friend");
  316. var info = resp.data;
  317. List<Map<String, dynamic>> list = [];
  318. if (info != null && info.records != null) {
  319. var records = info.records!;
  320. for (var i = 0; i < records.length; i++) {
  321. list.add({"user": records[i].toJsonSimple(), "score": records[i].score ?? 0, "rank": (i + 1)});
  322. }
  323. }
  324. _js("callback", "'getRank_callback', [$type, ${json.encode({"list": list})}]");
  325. } else if ("vibrate" == method) {
  326. if (this._vibrate != true) return;
  327. var args = data['args'];
  328. var duration = Converter.toInt(args['duration']);
  329. var leftOrRight = Converter.toInt(args['leftOrRight']);
  330. bluetooth.vibrate(duration, leftOrRight: leftOrRight);
  331. } else if ("openGameData" == method) {
  332. var args = data['args'];
  333. var open = args['open'] as bool;
  334. _openGameData = open;
  335. } else if ("openGameAtt" == method) {
  336. var args = data['args'];
  337. var open = args['open'] as bool;
  338. _openGameAtt = open;
  339. }
  340. }
  341. @override
  342. Widget build(BuildContext context) {
  343. var size = MediaQuery.of(context).size;
  344. String url = widget.game.downloadUrl!;
  345. // url = "http://172.16.14.128:7456/";
  346. var body = Scaffold(
  347. backgroundColor: Colors.black,
  348. body: Stack(
  349. fit: StackFit.expand,
  350. children: [
  351. Center(
  352. child: AspectRatio(
  353. aspectRatio: math.max(size.aspectRatio, 1.78),
  354. child: WebView(
  355. key: _key,
  356. backgroundColor: Colors.black,
  357. initialUrl: "$url",
  358. gestureNavigationEnabled: true,
  359. javascriptMode: JavascriptMode.unrestricted,
  360. initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
  361. allowsInlineMediaPlayback: true,
  362. onWebViewCreated: (WebViewController webViewController) {
  363. _controller = webViewController;
  364. bluetooth.setupGameMode4h5(true);
  365. },
  366. onProgress: (progress) {
  367. progressNotifier.value = progress;
  368. },
  369. onPageFinished: (r) {
  370. setState(() {
  371. _onPageFinished = true;
  372. });
  373. var sdkVersion = sdk.getVersion();
  374. if (isDebugShoe) ToastUtil.show("网页加载完成!GameSDK Version: $sdkVersion");
  375. // Future.delayed(Duration(seconds: 10)).then((value) {
  376. // if (_gamePlaying != true && _gameFinished != true) _onGameStart();
  377. // });
  378. },
  379. javascriptChannels: javascriptChannels,
  380. ),
  381. ),
  382. ),
  383. if (_onPageFinished == false)
  384. ValueListenableBuilder<int>(
  385. valueListenable: progressNotifier,
  386. builder: (context, v, _) {
  387. return Center(
  388. child: Column(
  389. mainAxisSize: MainAxisSize.min,
  390. children: [
  391. SizedBox(
  392. width: 100,
  393. height: 100,
  394. child: CircularProgressIndicator(
  395. value: v / 100.0,
  396. strokeWidth: 6,
  397. )),
  398. const SizedBox(height: 20.0,),
  399. Text("运动正在加载...", style: Theme.of(context).textTheme.headline6,)
  400. ],
  401. ));
  402. }),
  403. if (_onPageFinished == false)
  404. Positioned(
  405. top: 8,
  406. right: 8,
  407. child: IconButton(
  408. onPressed: () => Navigator.pop(context),
  409. icon: Icon(
  410. Icons.close,
  411. color: Colors.white,
  412. ))),
  413. // if (_onPageFinished == true && _onSdkLoaded == false)
  414. // Center(
  415. // child: SizedBox(
  416. // width: 100,
  417. // height: 100,
  418. // child: CircularProgressIndicator(
  419. // strokeWidth: 6,
  420. // ))),
  421. // if ((widget.game.h5 ?? 0) == 1 && MediaQuery.of(context).orientation == Orientation.portrait)
  422. // Center(
  423. // child: Column(
  424. // mainAxisSize: MainAxisSize.min,
  425. // children: [
  426. // Image.asset("lib/assets/img/icon_pop_horizontal_screen.png",
  427. // height: 60,
  428. // fit: BoxFit.fitHeight,
  429. // ),
  430. // const SizedBox(height: 16.0,),
  431. // Text("为了更好的体验,请将手机横过来", style: Theme.of(context).textTheme.headline6,)
  432. // ],
  433. // ),
  434. // ),
  435. ],
  436. ),
  437. );
  438. return Platform.isIOS
  439. ? body
  440. : WillPopScope(
  441. onWillPop: () async {
  442. return await showDialog(
  443. context: context,
  444. builder: (context) => CustomAlertDialog(title: '是否退出${widget.game.name}', ok: () => Navigator.of(context).pop(true)),
  445. ) ==
  446. true;
  447. },
  448. child: body,
  449. );
  450. }
  451. }