game_detail.dart 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:android_intent/android_intent.dart';
  5. import 'package:android_intent/flag.dart';
  6. import 'package:app_installer/app_installer.dart';
  7. import 'package:broadcast/broadcast.dart';
  8. import 'package:cached_network_image/cached_network_image.dart';
  9. import 'package:connectivity_plus/connectivity_plus.dart';
  10. import 'package:device_apps/device_apps.dart' as app;
  11. import 'package:dio/dio.dart';
  12. import 'package:flutter/material.dart';
  13. import 'package:flutter/services.dart';
  14. import 'package:flutter_html/flutter_html.dart';
  15. import 'package:flutter_swiper_plus/flutter_swiper_plus.dart';
  16. import 'package:get_it/get_it.dart';
  17. import 'package:package_info/package_info.dart';
  18. import 'package:permission_handler/permission_handler.dart';
  19. import 'package:provider/provider.dart';
  20. import 'package:shared_preferences/shared_preferences.dart';
  21. import 'package:sport/application.dart';
  22. import 'package:sport/bean/feed_back.dart';
  23. import 'package:sport/bean/game.dart';
  24. import 'package:sport/bean/image.dart' as photo;
  25. import 'package:sport/bean/rank_game_info.dart';
  26. import 'package:sport/bean/user_info.dart';
  27. import 'package:sport/pages/game/game_guide.dart';
  28. import 'package:sport/pages/game/rank_detail.dart';
  29. import 'package:sport/pages/my/feedback_detail_page.dart';
  30. import 'package:sport/pages/social/gallery_photo_view.dart';
  31. import 'package:sport/pages/social/post_comment.dart';
  32. import 'package:sport/pages/web/game_page.dart';
  33. import 'package:sport/pages/web/web_page.dart';
  34. import 'package:sport/provider/bluetooth.dart';
  35. import 'package:sport/provider/game_info_model.dart';
  36. import 'package:sport/provider/game_model.dart';
  37. import 'package:sport/provider/lib/provider_widget.dart';
  38. import 'package:sport/provider/login_info_model.dart';
  39. import 'package:sport/provider/user_model.dart';
  40. import 'package:sport/router/navigator_util.dart';
  41. import 'package:sport/services/api/inject_api.dart';
  42. import 'package:sport/services/api/resp.dart';
  43. import 'package:sport/services/app_lifecycle_state.dart';
  44. import 'package:sport/services/game_manager.dart';
  45. import 'package:sport/utils/DateFormat.dart';
  46. import 'package:sport/utils/download.dart';
  47. import 'package:sport/utils/toast.dart';
  48. import 'package:sport/utils/version.dart';
  49. import 'package:sport/widgets/appbar.dart';
  50. import 'package:sport/widgets/decoration.dart';
  51. import 'package:sport/widgets/dialog/alert_dialog.dart';
  52. import 'package:sport/widgets/dialog/bindphone_dialog.dart';
  53. import 'package:sport/widgets/dialog/request_dialog.dart';
  54. import 'package:sport/widgets/error.dart';
  55. import 'package:sport/widgets/image.dart';
  56. import 'package:sport/widgets/loading.dart';
  57. import 'package:sport/widgets/misc.dart';
  58. import 'package:sport/widgets/popmenu_bg.dart';
  59. import 'package:sport/widgets/text_input.dart' as TextInput;
  60. import 'package:umeng_common_sdk/umeng_common_sdk.dart';
  61. import 'package:url_launcher/url_launcher.dart';
  62. import 'package:video_player/video_player.dart';
  63. import 'package:visibility_detector/visibility_detector.dart';
  64. Future<bool> startGame(BuildContext context, GameInfoData? game, {String? invite = "", UserInfo? user, bool launch4GameCenter = false, bool skipGuide = false}) async {
  65. if (game == null) return false;
  66. Bluetooth bluetooth = GetIt.I<Bluetooth>();
  67. if (!bluetooth.isConnected) {
  68. ToastUtil.showBottom("请先连接智能鞋");
  69. return false;
  70. }
  71. if (bluetooth.electricityNotifier.value < 10) {
  72. ToastUtil.showBottom("鞋子电量不足,请及时充电!");
  73. }
  74. var gameType = game.gameType ?? 0;
  75. if (gameType == 0) {
  76. ToastUtil.showBottom("无效游戏类型 ${game.name}");
  77. return false;
  78. }
  79. var packageInfo = await PackageInfo.fromPlatform();
  80. var versionName = packageInfo.version;
  81. var v = versionCompare(versionName, game.c_ver ?? "");
  82. if (v < 0) {
  83. ToastUtil.showBottom("趣动App不支持该游戏 ${game.name},请升级至最新版本");
  84. return false;
  85. }
  86. String? mac = bluetooth.deviceId;
  87. SharedPreferences prefs = await SharedPreferences.getInstance();
  88. var userModel = Provider.of<UserModel>(context, listen: false);
  89. var userInfo = userModel.user;
  90. if (skipGuide != true) {
  91. //
  92. String baseKey = "guide_game_0";
  93. bool guide = prefs.getBool(baseKey) ?? true;
  94. // if (guide == true) {
  95. // var result = await NavigatorUtil.goPage(
  96. // context,
  97. // (context) => GameGuide(
  98. // game: game,
  99. // base: true,
  100. // launch4GameCenter: launch4GameCenter,
  101. // ));
  102. // if (result is List<dynamic> && result[0] == true) {
  103. // prefs.setBool(baseKey, !result[1]);
  104. // } else {
  105. // return;
  106. // }
  107. // }
  108. // //
  109. // // prefs.getKeys().forEach((element) {
  110. // // if(element.startsWith("guide")){
  111. // // prefs.setBool(element, true);
  112. // // }
  113. // // });
  114. //
  115. if (guideActionMap.containsKey(game.id)) {
  116. String key = "guide_game_${game.id}";
  117. // guide = prefs.getBool(key) ?? true;
  118. guide = userInfo.setting[key] ?? true;
  119. // guide = true;
  120. if (guide == true) {
  121. var result = await NavigatorUtil.goPageRemoveUntil(
  122. context,
  123. (context) => GameGuide(
  124. game: game,
  125. launch4GameCenter: launch4GameCenter,
  126. ),
  127. settings: RouteSettings(name: "game_guide"),
  128. );
  129. if (result is List<dynamic> && result[0] == true) {
  130. // await prefs.setBool(key, !result[1]);
  131. userInfo.setting[key] = !result[1];
  132. LoginInfoModel loginInfoModel = GetIt.I();
  133. loginInfoModel.loginApi.clientConfig(json.encode(userInfo.setting));
  134. loginInfoModel.saveUser(context, userInfo);
  135. await request(context, () async {
  136. await Future.delayed(Duration(seconds: 2));
  137. });
  138. if (!launch4GameCenter && (game.h5 ?? 0) == 0) {
  139. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  140. }
  141. } else {
  142. return false;
  143. }
  144. }
  145. }
  146. }
  147. var userMap = userInfo.toJsonSimple();
  148. String token = prefs.getString("token") ?? "";
  149. var params = {
  150. 'token': token,
  151. 'mac': "$mac",
  152. 'game_id': "${game.id}",
  153. 'game_type': "$gameType",
  154. 'user_id': "${userInfo.id}",
  155. 'user': userMap,
  156. 'vibrate': prefs.containsKey("vibrate") ? prefs.getBool("vibrate") : true,
  157. if (invite?.isNotEmpty == true) 'invite': {"info": "$invite", "user": user?.toJsonSimple() ?? {}, "game_id": "${game.id}", "ts": DateTime.now().millisecondsSinceEpoch},
  158. 'debug_game_mode': isDebugShoe
  159. ? prefs.containsKey("debug_game")
  160. ? prefs.getBool("debug_game")
  161. : true
  162. : false,
  163. 'debug': isDebugShoe,
  164. };
  165. print("start game $gameType params: $params");
  166. GetIt.I<GameModel>().addGame(game);
  167. if ((game.h5 ?? 0) > 0 && game.downloadUrl != null) {
  168. await NavigatorUtil.goPageRemoveUntil(context, (context) => GamePage(game: game, gameCenter: launch4GameCenter), settings: RouteSettings(name: "game"));
  169. return true;
  170. }
  171. if (Platform.isAndroid) {
  172. if (game.packageNameAndroid == null) {
  173. return false;
  174. }
  175. var sport = await app.DeviceApps.getApp(game.packageNameAndroid!);
  176. if (sport == null) {
  177. ToastUtil.showBottom("你还没安装 ${game.name} ${game.packageNameAndroid}");
  178. return false;
  179. }
  180. Broadcast.broadcast("xie.hiyd.com.GameService");
  181. ToastUtil.showBottom("正在打开... ${game.name} ${game.packageNameAndroid}");
  182. // bluetooth.disconnectDevice("游戏开始,主动断开");
  183. AndroidIntent intent = AndroidIntent(action: "android.intent.action.MAIN", package: game.packageNameAndroid, componentName: "com.ouj.shoe.sdklibrary.GameActivity", data: "oujgame://com.ouj.shoe", flags: [Flag.FLAG_ACTIVITY_NEW_TASK], arguments: params);
  184. await intent.launch();
  185. return true;
  186. // app.DeviceApps.openApp(_data.packageNameAndroid)
  187. } else if (Platform.isIOS) {
  188. var url = "${game.packageNameIos}://${Uri.encodeComponent(json.encode(params))}";
  189. //
  190. // launch(url);
  191. bluetooth.disconnectDevice("游戏开始,主动断开");
  192. bool _canLaunch = await canLaunch(url);
  193. if (_canLaunch) {
  194. await launch(url);
  195. } else {
  196. if (game.appstore != null) {
  197. await launch(game.appstore!);
  198. }
  199. }
  200. // ToastUtil.show("正在打开... ${game.name} ${game.packageNameIos}");
  201. return true;
  202. }
  203. return true;
  204. }
  205. Future<int> isInstalled(GameInfoData game) async {
  206. if ((game.h5 ?? 0) > 0) {
  207. return 0;
  208. }
  209. if (Platform.isAndroid) {
  210. if (game.packageNameAndroid?.isNotEmpty == true && (await app.DeviceApps.isAppInstalled(game.packageNameAndroid!))) return 0;
  211. } else if (Platform.isIOS) {
  212. var url = "${game.packageNameIos}://${Uri.encodeComponent(json.encode("{}"))}";
  213. bool _canLaunch = await canLaunch(url);
  214. if (_canLaunch) {
  215. return 0;
  216. }
  217. }
  218. return 1;
  219. }
  220. final Map<int, String> GAME_RULE = {1: "qdtw", 33: "tgds", 22: "jdstb", 3: "qdpk", 10: "gfxy", 9: "tydb"};
  221. class GameDetailsPage extends StatefulWidget {
  222. final GameInfoData details;
  223. GameDetailsPage(this.details);
  224. @override
  225. State<StatefulWidget> createState() {
  226. return _GameDetailPageState();
  227. }
  228. }
  229. class _GameDetailPageState extends State<GameDetailsPage> with TickerProviderStateMixin, InjectApi, InjectLoginApi {
  230. FocusNode _comment = FocusNode();
  231. bool isChangeFullScreen = false;
  232. late CommentListModel _commentListModel;
  233. ValueNotifier<int> _swiperIndex = ValueNotifier(0);
  234. bool defaultLike = false;
  235. bool isLike = false;
  236. late int id;
  237. final GlobalKey _videoKey = GlobalKey();
  238. @override
  239. void initState() {
  240. super.initState();
  241. id = widget.details.id;
  242. defaultLike = isLike = Application.gameLikes[id] ?? widget.details.isLike ?? false;
  243. _commentListModel = new CommentListModel('${widget.details.subjectId}', by: 'created_at');
  244. }
  245. @override
  246. void dispose() {
  247. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((value) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]));
  248. super.dispose();
  249. _commentListModel.dispose();
  250. _swiperIndex.dispose();
  251. }
  252. Future<bool> _checkInstall() async {
  253. if (widget.details.packageNameAndroid?.isNotEmpty == true) return await app.DeviceApps.isAppInstalled(widget.details.packageNameAndroid ?? "");
  254. return false;
  255. }
  256. fullScreen(bool change) {
  257. setState(() {
  258. isChangeFullScreen = change;
  259. });
  260. }
  261. @override
  262. Widget build(BuildContext context) {
  263. final mediaQuery = MediaQuery.of(context);
  264. final double statusBarHeight = mediaQuery.padding.top;
  265. final double pinnedHeaderHeight = statusBarHeight + kToolbarHeight;
  266. GameInfoData game = widget.details;
  267. if (isChangeFullScreen == true) {
  268. return WillPopScope(
  269. onWillPop: () async {
  270. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  271. return false;
  272. },
  273. child: Scaffold(backgroundColor: Colors.black, body: Center(child: AspectRatio(aspectRatio: 1.78, child: _VideoWidget(key: _videoKey, isFullScreen: true, data: game)))),
  274. );
  275. }
  276. int imagesLength = widget.details.introduceImages?.length ?? 0;
  277. int swiperLength = imagesLength;
  278. if (game.introduceVideo?.isNotEmpty == true) {
  279. swiperLength += 1;
  280. }
  281. return Scaffold(
  282. backgroundColor: Colors.white,
  283. appBar: buildAppBar(
  284. context,
  285. title: "运动详情",
  286. actions: [
  287. FutureBuilder<bool>(
  288. future: _checkInstall(),
  289. builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
  290. var list = [
  291. if (GAME_RULE.containsKey(widget.details.id)) menuItem('游戏规则', 'linkpop_icon_record.png', '游戏规则'),
  292. menuItem('运动记录', 'linkpop_icon_motion.png', '运动记录'),
  293. // menuItem('操作指引', 'linkpop_icon_motion.png', '操作指引'),
  294. menuItem('问题反馈', 'linkpop_icon_feedback.png', '问题反馈'),
  295. ];
  296. if (snapshot.data == true && Platform.isAndroid && widget.details.h5 == 0) {
  297. list.add(menuItem('删除运动', 'linkpop_icon_del.png', '删除运动'));
  298. }
  299. return PopupMenuTheme(
  300. data: PopupMenuThemeData(shape: PopmenuShape(borderRadius: BorderRadius.all(Radius.circular(10.0)))),
  301. child: PopupMenuButton(
  302. offset: Offset(0, kToolbarHeight / 2 + 15),
  303. icon: iconMoreGray(),
  304. onSelected: (val) {
  305. _onPopMenuSelected(val);
  306. },
  307. itemBuilder: (context) {
  308. return divideMenus(list);
  309. },
  310. ));
  311. }),
  312. ],
  313. ),
  314. body: ProviderWidget<CommentListModel>(
  315. model: _commentListModel,
  316. onModelReady: (model) => model.initData(),
  317. autoDispose: false,
  318. builder: (_, model, __) {
  319. return CustomScrollView(
  320. slivers: [
  321. SliverToBoxAdapter(
  322. child: Container(
  323. child: Column(
  324. children: [
  325. if (swiperLength > 0)
  326. AspectRatio(
  327. aspectRatio: 1.8,
  328. child: Stack(
  329. fit: StackFit.expand,
  330. children: [
  331. Swiper(
  332. itemCount: swiperLength,
  333. itemBuilder: (BuildContext context, int index) {
  334. var padding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0);
  335. if (imagesLength != swiperLength && index == 0) {
  336. return Padding(
  337. padding: padding,
  338. child: AspectRatio(aspectRatio: 1.78, child: ClipRRect(borderRadius: BorderRadius.circular(10.0), child: _VideoWidget(key: _videoKey, isFullScreen: false, data: game))),
  339. );
  340. } else {
  341. int _i = index - (imagesLength != swiperLength ? 1 : 0);
  342. List<String> _images = widget.details.introduceImages ?? [];
  343. if (_images.isEmpty) return Container();
  344. return InkWell(
  345. onTap: () {
  346. Navigator.push(
  347. context,
  348. FadeRoute(
  349. page: GalleryPhotoViewWrapper(
  350. galleryItems: _images.map((e) => photo.Image(id: "${_images.indexOf(e)}", src: e)).toList(),
  351. backgroundDecoration: const BoxDecoration(
  352. color: Colors.black,
  353. ),
  354. initialIndex: _i,
  355. scrollDirection: Axis.horizontal,
  356. ),
  357. ),
  358. );
  359. },
  360. child: Padding(
  361. padding: padding,
  362. child: ClipRRect(
  363. borderRadius: BorderRadius.circular(10.0),
  364. child: Container(
  365. color: Colors.black,
  366. child: CachedNetworkImage(
  367. imageUrl: _images[_i],
  368. fit: BoxFit.cover,
  369. ),
  370. ),
  371. ),
  372. ),
  373. );
  374. }
  375. },
  376. loop: false,
  377. onIndexChanged: (i) => _swiperIndex.value = i,
  378. ),
  379. Positioned(
  380. bottom: 20.0,
  381. right: 28.0,
  382. child: Container(
  383. padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0),
  384. decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), color: Colors.black.withOpacity(.5)),
  385. child: Row(
  386. mainAxisSize: MainAxisSize.min,
  387. children: [
  388. ValueListenableBuilder<int>(
  389. valueListenable: _swiperIndex,
  390. builder: (context, snapshot, _) {
  391. return Text(
  392. "${snapshot + 1}",
  393. style: Theme.of(context).textTheme.subtitle2!.copyWith(color: Colors.white),
  394. );
  395. }),
  396. Text(
  397. "/$swiperLength",
  398. style: Theme.of(context).textTheme.subtitle2!.copyWith(color: Colors.white),
  399. ),
  400. ],
  401. ),
  402. ),
  403. ),
  404. ],
  405. ),
  406. ),
  407. Padding(
  408. padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
  409. child: Column(
  410. crossAxisAlignment: CrossAxisAlignment.start,
  411. children: [
  412. Row(
  413. children: <Widget>[
  414. Container(
  415. margin: EdgeInsets.only(right: 10.0),
  416. decoration: ShapeDecoration(image: DecorationImage(image: CachedNetworkImageProvider(widget.details.cover ?? ''), fit: BoxFit.cover), shape: RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.circular(6))),
  417. width: 60.0,
  418. height: 60.0,
  419. ),
  420. Expanded(
  421. child: Column(
  422. crossAxisAlignment: CrossAxisAlignment.start,
  423. children: [
  424. Text(
  425. "${widget.details.name}",
  426. style: Theme.of(context).textTheme.headline1!,
  427. ),
  428. const SizedBox(
  429. height: 12,
  430. ),
  431. if (game.tags != null)
  432. Wrap(
  433. spacing: 4,
  434. runSpacing: 4,
  435. children: game.tags!.map((e) => gameTag(context, e)).toList(),
  436. ),
  437. ],
  438. ),
  439. ),
  440. // InkWell(
  441. // child: Row(
  442. // children: <Widget>[
  443. // Image.asset(
  444. // isLike ? "lib/assets/img/bbs_icon_like_complete.png" : "lib/assets/img/bbs_icon_like.png",
  445. // width: 26,
  446. // height: 38,
  447. // ),
  448. // Padding(
  449. // padding: EdgeInsets.symmetric(horizontal: 5.0),
  450. // child: Text(
  451. // "${max(0, (widget.details.likeCount ?? 0) + (isLike == true ? defaultLike ? 0 : 1 : defaultLike ? -1 : 0))}",
  452. // style: Theme.of(context).textTheme.subtitle2,
  453. // ),
  454. // )
  455. // ],
  456. // ),
  457. // onTap: throttle(() async {
  458. // var data = await request(context, () async {
  459. // RespData<String> data;
  460. // if (!isLike) {
  461. // data = await api.postForumLike('$id', 'game_id');
  462. // } else {
  463. // data = await api.postForumUnLike('$id', 'game_id');
  464. // }
  465. // return data;
  466. // });
  467. // if (data?.code == 0) {
  468. // setState(() {
  469. // isLike = !isLike;
  470. // Application.gameLikes[id] = isLike;
  471. // });
  472. // }
  473. // }),
  474. // )
  475. FutureProvider<Rank?>(
  476. initialData: null,
  477. create: (context) async {
  478. Rank? _rank = game.rank;
  479. if (_rank == null) {
  480. RespList<GameInfoData> resp = await api.getGameAll();
  481. _rank = resp.results.firstWhere((element) => element.id == id).rank;
  482. }
  483. return _rank;
  484. },
  485. builder: (context, child) {
  486. return InkWell(
  487. child: Padding(
  488. padding: const EdgeInsets.symmetric(horizontal: 6.0),
  489. child: Column(
  490. children: <Widget>[
  491. Padding(
  492. padding: const EdgeInsets.all(8.0),
  493. child: Image.asset("lib/assets/img/rank_icon_game.png"),
  494. ),
  495. Text(
  496. "排行榜",
  497. style: Theme.of(context).textTheme.subtitle1,
  498. ),
  499. ],
  500. ),
  501. ),
  502. onTap: () => NavigatorUtil.goPage(context, (context) => RankDetailPage(id, 1)),
  503. );
  504. },
  505. ),
  506. ],
  507. ),
  508. Container(
  509. margin: EdgeInsets.only(top: 24.0, bottom: 10.0),
  510. child: StartButtonWidget(game, (start) async {
  511. startGame(context, widget.details);
  512. UmengCommonSdk.onEvent("game_start_detail", {'id': '${game.id}'});
  513. })),
  514. Padding(
  515. padding: const EdgeInsets.fromLTRB(0, 24, 0, 12),
  516. child: Text("互动规则", style: Theme.of(context).textTheme.headline3),
  517. ),
  518. Html(
  519. shrinkWrap: true,
  520. data: "${game.guide}",
  521. style: {"strong": Style(fontWeight: FontWeight.normal), "li": Style(margin: const EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.zero)},
  522. ),
  523. if (guideActionMap.containsKey(game.id))
  524. InkWell(
  525. onTap: () {
  526. NavigatorUtil.goPage(
  527. context,
  528. (context) => GameGuide(
  529. game: game,
  530. detailGuide: true,
  531. ));
  532. },
  533. child: Container(
  534. decoration: BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor, borderRadius: BorderRadius.circular(10)),
  535. padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 14.0),
  536. margin: const EdgeInsets.symmetric(vertical: 12.0),
  537. child: Center(
  538. child: Row(
  539. mainAxisSize: MainAxisSize.min,
  540. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  541. children: [
  542. Text("互动教程", style: Theme.of(context).textTheme.subtitle1),
  543. Padding(
  544. padding: const EdgeInsets.only(left: 6.0),
  545. child: arrowRight(),
  546. ),
  547. ],
  548. ),
  549. ),
  550. ),
  551. ),
  552. Padding(
  553. padding: const EdgeInsets.fromLTRB(0, 24, 0, 12),
  554. child: Text("运动介绍", style: Theme.of(context).textTheme.headline3),
  555. ),
  556. Text(
  557. "${game.introduce}",
  558. style: Theme.of(context).textTheme.bodyText2!.copyWith(height: 1.8),
  559. ),
  560. ],
  561. ),
  562. ),
  563. ],
  564. ),
  565. ),
  566. ),
  567. if (openSocial())
  568. SliverToBoxAdapter(
  569. child: Column(
  570. crossAxisAlignment: CrossAxisAlignment.start,
  571. children: [
  572. Padding(
  573. padding: const EdgeInsets.fromLTRB(16, 24, 16, 12),
  574. child: Text("游戏评论", style: Theme.of(context).textTheme.headline3),
  575. ),
  576. Container(
  577. margin: const EdgeInsets.fromLTRB(16, 12, 16, 24),
  578. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14.0),
  579. decoration: card(),
  580. child: InkWell(
  581. child: Row(
  582. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  583. crossAxisAlignment: CrossAxisAlignment.center,
  584. children: <Widget>[
  585. Row(
  586. children: <Widget>[
  587. Consumer<UserModel>(
  588. builder: (_, model, __) {
  589. return Row(
  590. children: <Widget>[
  591. CircleAvatar(backgroundColor: Colors.black26, radius: 15, backgroundImage: CachedNetworkImageProvider(model.user.avatar ?? "")),
  592. SizedBox(
  593. width: 12,
  594. ),
  595. Text(model.user.name, style: Theme.of(context).textTheme.subtitle1!.copyWith(fontWeight: FontWeight.w600))
  596. ],
  597. );
  598. },
  599. ),
  600. ],
  601. ),
  602. Row(
  603. children: <Widget>[
  604. Padding(
  605. padding: EdgeInsets.only(right: 6.0),
  606. child: Text(
  607. "发表评论",
  608. style: TextStyle(color: Color.fromRGBO(51, 51, 51, 1)),
  609. ),
  610. ),
  611. arrowRight(),
  612. ],
  613. )
  614. ],
  615. ),
  616. onTap: () async {
  617. if (await showBindPhoneDialog(context) != true) {
  618. return;
  619. }
  620. // 从下面弹出个框来 输入 评论
  621. showModalBottomSheet(
  622. context: context,
  623. isScrollControlled: true, // !important
  624. builder: (BuildContext context) {
  625. return SingleChildScrollView(
  626. child: Container(
  627. padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), // !important
  628. child: TextInput.TextInput(
  629. '${widget.details.subjectId}',
  630. focusNode: _comment,
  631. autoFocus: true,
  632. callback: () {
  633. model.refresh();
  634. Navigator.pop(context);
  635. },
  636. )),
  637. );
  638. },
  639. );
  640. },
  641. ),
  642. ),
  643. ],
  644. ),
  645. ),
  646. if (openSocial() && model.isBusy)
  647. SliverToBoxAdapter(
  648. child: RequestLoadingWidget(),
  649. ),
  650. if (openSocial() && model.isIdle && model.list.isNotEmpty)
  651. SliverPadding(
  652. padding: const EdgeInsets.symmetric(horizontal: 8),
  653. sliver: SliverList(
  654. delegate: SliverChildBuilderDelegate((context, index) {
  655. return PostCommentWidget(model.list[index]);
  656. }, childCount: model.list.length)),
  657. ),
  658. if (openSocial() && model.isEmpty)
  659. SliverToBoxAdapter(
  660. child: Container(
  661. child: Center(
  662. child: RequestErrorWidget(
  663. null,
  664. msg: "暂无评论~",
  665. assets: RequestErrorWidget.ASSETS_NO_COMMENT,
  666. ),
  667. ),
  668. ),
  669. ),
  670. if (model.isIdle && model.list.isNotEmpty)
  671. SliverToBoxAdapter(
  672. child: Center(
  673. child: Padding(
  674. padding: const EdgeInsets.all(32.0),
  675. child: Text("没有更多数据了"),
  676. )),
  677. ),
  678. ],
  679. );
  680. },
  681. ),
  682. );
  683. }
  684. _onPopMenuSelected(var val) async {
  685. switch (val) {
  686. case '运动记录':
  687. NavigatorUtil.goGameHistory(context, widget.details);
  688. break;
  689. case '游戏规则':
  690. NavigatorUtil.goPage(context, (context) => WebPage(url: "https://xie-web.funfet.com/index.html#/pages/Rules/index?name=${GAME_RULE[widget.details.id]}"));
  691. break;
  692. // case '操作指引':
  693. // NavigatorUtil.goPage(context, (context) => WebPage(url: "http://xie-web.hiyd.com/index.html#/game_guide?id=${widget.details.id}"));
  694. // break;
  695. case '问题反馈':
  696. FeedTypeInfoData? group;
  697. await request(context, () async {
  698. FeedTypeInfo _feedTypeInfo = await loginApi.getFeedBackTypes();
  699. if (_feedTypeInfo.code == 0) {
  700. group = _feedTypeInfo.data?.singleWhere((element) => element.groupId == '3');
  701. }
  702. });
  703. if (group != null) {
  704. NavigatorUtil.goPage(context, (context) => FeedbackDetailPage(group!));
  705. }
  706. break;
  707. case '删除运动':
  708. if (Platform.isAndroid) {
  709. var installed = await app.DeviceApps.isAppInstalled(widget.details.packageNameAndroid ?? "");
  710. if (installed == true) {
  711. if (await showDialog(
  712. context: context,
  713. builder: (context) => CustomAlertDialog(title: '是否删除运动', ok: () => Navigator.of(context).pop(true)),
  714. ) ==
  715. true) {
  716. GameManager.deleteFile(widget.details);
  717. await app.DeviceApps.uninstallApp(widget.details.packageNameAndroid ?? "");
  718. setState(() {});
  719. }
  720. } else {}
  721. }
  722. break;
  723. }
  724. }
  725. }
  726. // bottom Button
  727. class StartButtonWidget extends StatefulWidget {
  728. final GameInfoData data;
  729. final Function startCallBack;
  730. final double width, height;
  731. final TextStyle textStyle;
  732. StartButtonWidget(this.data, this.startCallBack,
  733. {this.width = double.infinity,
  734. this.height = 44,
  735. this.textStyle = const TextStyle(
  736. color: Colors.white,
  737. fontSize: 14.0,
  738. fontWeight: FontWeight.w600,
  739. height: 1.1,
  740. )});
  741. @override
  742. State<StatefulWidget> createState() {
  743. return _StartButtonWidgetState();
  744. }
  745. }
  746. class _StartButtonWidgetState extends LifecycleState<StartButtonWidget> {
  747. DownStatus _downStatus = DownStatus.unknown;
  748. double _percent = 0.0;
  749. late StreamSubscription _subscription;
  750. @override
  751. void initState() {
  752. super.initState();
  753. _subscription = DownLoadManage().stream.listen((event) {
  754. if (event.tag != widget.data.downloadUrl!) return;
  755. if (event.done == true) {
  756. _downStatus = DownStatus.install;
  757. ToastUtil.showBottom("下载成功,请按步骤继续安装");
  758. _installApk();
  759. } else if (event.failed == true) {
  760. if (event.error is DioError && event.error?.type == DioErrorType.cancel) {
  761. _downStatus = DownStatus.pause;
  762. } else {
  763. ToastUtil.showBottom("下载失败,请重试 ${event.error.toString()}");
  764. _downStatus = DownStatus.unsupported;
  765. }
  766. } else {
  767. if (event.total != -1) {
  768. _percent = (event.count / event.total * 100);
  769. }
  770. }
  771. if (mounted) setState(() {});
  772. });
  773. _isReady().then((value) => setState(() {}));
  774. }
  775. @override
  776. void dispose() {
  777. if (DownLoadManage().isDowning(widget.data.downloadUrl!)) {
  778. ToastUtil.showBottom("后台下载中...");
  779. }
  780. super.dispose();
  781. _subscription.cancel();
  782. // stopDownload();
  783. }
  784. @override
  785. Future didChangeAppLifecycleState(AppLifecycleState state) async {
  786. super.didChangeAppLifecycleState(state);
  787. if (state == AppLifecycleState.resumed) {
  788. _isReady().then((value) {
  789. if (mounted) setState(() {});
  790. });
  791. }
  792. }
  793. Future _isReady() async {
  794. _downStatus = DownStatus.unsupported;
  795. GameInfoData game = widget.data;
  796. if ((game.h5 ?? 0) > 0) {
  797. _downStatus = DownStatus.ready;
  798. return;
  799. }
  800. if (Platform.isAndroid && widget.data.packageNameAndroid?.isNotEmpty == true) {
  801. if (DownLoadManage().isDowning(widget.data.downloadUrl!)) {
  802. _downStatus = DownStatus.down;
  803. } else {
  804. var sport = await app.DeviceApps.getApp(widget.data.packageNameAndroid ?? "");
  805. if (sport != null) {
  806. var v = versionCompare(sport.versionName ?? "", widget.data.version ?? "");
  807. // print("111111111 $sport $v");
  808. if (v >= 0) {
  809. _downStatus = DownStatus.ready;
  810. } else {
  811. _downStatus = DownStatus.update;
  812. }
  813. } else {
  814. int length = await _checkDownload();
  815. // print("11111111111111 length: $length");
  816. if (length > 0) {
  817. _downStatus = DownStatus.pause;
  818. // print("11111111111111 length: $length, total: ${game.fileSizeByte} $_percent");
  819. _percent = (length / (game.fileSizeByte ?? 0)) * 100;
  820. if (_percent >= 100) {
  821. _downStatus = DownStatus.install;
  822. }
  823. }
  824. }
  825. }
  826. } else if (Platform.isIOS) {
  827. var url = "${game.packageNameIos}://${Uri.encodeComponent(json.encode("{}"))}";
  828. bool _canLaunch = await canLaunch(url);
  829. if (_canLaunch) {
  830. _downStatus = DownStatus.ready;
  831. }
  832. }
  833. print("Game -- $_downStatus");
  834. }
  835. _installApk() async {
  836. if (Platform.isAndroid) {
  837. File? savePath = await GameManager.createFile(widget.data);
  838. if (savePath == null) {
  839. ToastUtil.showBottom("创建文件失败");
  840. return;
  841. }
  842. try {
  843. String path = savePath.path;
  844. AppInstaller.installApk(path).then((value) async {
  845. // var sport = await app.DeviceApps.getApp(widget.data.packageNameAndroid ?? "");
  846. // if (sport != null) {
  847. // GameManager.deleteFile(widget.data);
  848. // }
  849. });
  850. print('install apk $path');
  851. } catch (e) {
  852. print(e);
  853. print('install apk error: $e');
  854. }
  855. }
  856. }
  857. Future<int> _checkDownload() async {
  858. File? savePath = await GameManager.createFile(widget.data);
  859. if (savePath == null || !await savePath.exists()) {
  860. return 0;
  861. }
  862. int total = 0;
  863. try {
  864. final List<FileSystemEntity> children = savePath.parent.listSync();
  865. for (final FileSystemEntity file in children) {
  866. if (file is File) {
  867. int length = await file.length();
  868. total += length;
  869. }
  870. }
  871. } catch (e) {
  872. // print(e);
  873. }
  874. return total;
  875. }
  876. Future<bool> _startDownload() async {
  877. Bluetooth bluetooth = GetIt.I<Bluetooth>();
  878. if (bluetooth.isNotReady) {
  879. ToastUtil.showBottom("请先连接智能鞋");
  880. return false;
  881. }
  882. bool storage = await Application.requestPermission(Permission.storage);
  883. if (!storage) {
  884. ToastUtil.showBottom("没有下载文件权限");
  885. return false;
  886. }
  887. File? savePath = await GameManager.createFile(widget.data);
  888. if (savePath == null) {
  889. ToastUtil.showBottom("创建文件失败");
  890. return false;
  891. }
  892. if (_percent == 0) {
  893. await GameManager.deleteFile(widget.data);
  894. if (connectivityResult == ConnectivityResult.none) {
  895. ToastUtil.showBottom("请打开手机网络");
  896. return false;
  897. }
  898. var ok = await showDialog(
  899. context: context,
  900. builder: (context) => CustomAlertDialog(
  901. title: '下载${widget.data.name}到本地',
  902. child: Padding(
  903. padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0),
  904. child: Align(
  905. alignment: Alignment.centerLeft,
  906. child: Text(
  907. "当前使用${connectivityResult == ConnectivityResult.wifi ? 'WIFI' : '移动'}网络\n下载运动需要${widget.data.fileSize}M",
  908. style: Theme.of(context).textTheme.subtitle1?.copyWith(height: 1.8),
  909. ))),
  910. ok: () => Navigator.of(context).pop(true)),
  911. );
  912. if (ok != true) {
  913. return false;
  914. }
  915. }
  916. print("Game -- down: ${widget.data.downloadUrl}");
  917. DownLoadManage().download(widget.data.downloadUrl, savePath.path);
  918. return true;
  919. }
  920. stopDownload() {
  921. DownLoadManage().stop(widget.data.downloadUrl ?? "");
  922. }
  923. Widget _buildGameDownloadProgress() {
  924. return Container(
  925. height: widget.height,
  926. child: Stack(
  927. children: <Widget>[
  928. Container(
  929. decoration: BoxDecoration(
  930. borderRadius: BorderRadius.all(Radius.circular(20.0)),
  931. border: new Border.all(width: 1, color: Theme.of(context).colorScheme.secondary),
  932. ),
  933. child: ClipRRect(
  934. borderRadius: BorderRadius.circular(22.0),
  935. child: SizedBox(
  936. height: widget.height,
  937. child: LinearProgressIndicator(
  938. // backgroundColor: Color.fromRGBO(220, 220, 220, 1),
  939. backgroundColor: Colors.white,
  940. valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.secondary.withOpacity(0.3)),
  941. value: _percent / 100,
  942. ),
  943. ),
  944. ),
  945. ),
  946. Container(
  947. alignment: Alignment.center,
  948. child: Text(
  949. _percent >= 100.0 ? "正在校验" : "${_percent.toStringAsFixed(1)}%",
  950. style: widget.textStyle.copyWith(color: Theme.of(context).colorScheme.secondary),
  951. ),
  952. )
  953. ],
  954. ),
  955. );
  956. }
  957. _click() async {
  958. switch (_downStatus) {
  959. case DownStatus.ready:
  960. widget.startCallBack(1);
  961. break;
  962. case DownStatus.unknown:
  963. break;
  964. case DownStatus.update:
  965. case DownStatus.unsupported:
  966. case DownStatus.pause:
  967. if (Platform.isIOS) {
  968. launch(widget.data.appstore ?? "");
  969. } else if (Platform.isAndroid) {
  970. if (await _startDownload()) {
  971. _downStatus = DownStatus.down;
  972. }
  973. }
  974. break;
  975. case DownStatus.unauthorized:
  976. // TODO: Handle this case.
  977. break;
  978. case DownStatus.down:
  979. stopDownload();
  980. _downStatus = DownStatus.pause;
  981. break;
  982. case DownStatus.install:
  983. await _installApk();
  984. break;
  985. }
  986. setState(() {});
  987. }
  988. @override
  989. Widget build(BuildContext context) {
  990. int viewIndex = _downStatus.index;
  991. double height = widget.height;
  992. return GestureDetector(
  993. onTap: _click,
  994. child: IndexedStack(index: viewIndex, alignment: Alignment.center, children: <Widget>[
  995. CircularProgressIndicator(),
  996. _ButtonStyle(
  997. child: Text(
  998. "开始运动",
  999. style: widget.textStyle,
  1000. ),
  1001. height: height),
  1002. _ButtonStyle(
  1003. child: Platform.isAndroid
  1004. ? Row(
  1005. mainAxisSize: MainAxisSize.min,
  1006. children: [
  1007. Image.asset(
  1008. "lib/assets/img/btn_icon_download.png",
  1009. width: 16,
  1010. ),
  1011. SizedBox(
  1012. width: 5,
  1013. ),
  1014. Text(
  1015. "开始下载",
  1016. style: widget.textStyle,
  1017. ),
  1018. // SizedBox(
  1019. // width: 30,
  1020. // ),
  1021. ],
  1022. )
  1023. : Text(
  1024. "去下载",
  1025. style: widget.textStyle,
  1026. ),
  1027. height: height),
  1028. _ButtonStyle(
  1029. child: Row(
  1030. mainAxisSize: MainAxisSize.min,
  1031. children: [
  1032. Image.asset(
  1033. "lib/assets/img/btn_icon_download.png",
  1034. width: 16,
  1035. ),
  1036. SizedBox(
  1037. width: 5,
  1038. ),
  1039. Text(
  1040. "开始更新",
  1041. style: widget.textStyle,
  1042. ),
  1043. // SizedBox(
  1044. // width: 30,
  1045. // ),
  1046. ],
  1047. ),
  1048. height: height),
  1049. _buildGameDownloadProgress(),
  1050. _ButtonStyle(
  1051. child: Row(
  1052. mainAxisSize: MainAxisSize.min,
  1053. children: [
  1054. Image.asset(
  1055. "lib/assets/img/btn_icon_download.png",
  1056. width: 16,
  1057. ),
  1058. SizedBox(
  1059. width: 5,
  1060. ),
  1061. Text(
  1062. "继续下载",
  1063. style: widget.textStyle,
  1064. ),
  1065. // SizedBox(
  1066. // width: 30,
  1067. // ),
  1068. ],
  1069. ),
  1070. height: height),
  1071. _ButtonStyle(
  1072. child: Row(
  1073. mainAxisSize: MainAxisSize.min,
  1074. children: [
  1075. Image.asset(
  1076. "lib/assets/img/btn_icon_download.png",
  1077. width: 16,
  1078. ),
  1079. SizedBox(
  1080. width: 5,
  1081. ),
  1082. Text(
  1083. "安装运动",
  1084. style: widget.textStyle,
  1085. ),
  1086. // SizedBox(
  1087. // width: 30,
  1088. // ),
  1089. ],
  1090. ),
  1091. height: height),
  1092. Container(),
  1093. ]));
  1094. }
  1095. }
  1096. enum DownStatus {
  1097. unknown,
  1098. ready,
  1099. unsupported,
  1100. update,
  1101. down,
  1102. pause,
  1103. install,
  1104. unauthorized,
  1105. }
  1106. class _ButtonStyle extends StatelessWidget {
  1107. final Widget child;
  1108. final double height;
  1109. const _ButtonStyle({Key? key, required this.child, required this.height}) : super(key: key);
  1110. @override
  1111. Widget build(BuildContext context) {
  1112. return Container(
  1113. alignment: Alignment.center,
  1114. height: this.height,
  1115. decoration: BoxDecoration(
  1116. borderRadius: BorderRadius.all(Radius.circular(50.0)),
  1117. // border: new Border.all(
  1118. // width: 1, color: Theme.of(context).accentColor),
  1119. // color: Theme.of(context).accentColor,
  1120. boxShadow: [BoxShadow(offset: Offset(0.0, 3), blurRadius: 3, spreadRadius: 0, color: Color(0x82FF9100))],
  1121. gradient: new LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [
  1122. Color.fromRGBO(255, 196, 0, 1),
  1123. Color.fromRGBO(255, 170, 0, 1),
  1124. ]),
  1125. ),
  1126. // "开始下载(${widget.data.fileSize}M)"
  1127. child: child);
  1128. }
  1129. }
  1130. class _VideoWidget extends StatefulWidget {
  1131. final bool isFullScreen;
  1132. final GameInfoData data;
  1133. _VideoWidget({Key? key, required this.isFullScreen, required this.data}) : super(key: key);
  1134. @override
  1135. State<StatefulWidget> createState() {
  1136. return __VideoWidgetState();
  1137. }
  1138. }
  1139. class __VideoWidgetState extends State<_VideoWidget> {
  1140. Timer? timer;
  1141. var opacity = 1.0;
  1142. late VideoPlayerController controller; // 播放的controller
  1143. @override
  1144. void initState() {
  1145. super.initState();
  1146. controller = VideoPlayerController.network(widget.data.introduceVideo ?? "");
  1147. controller.setLooping(true);
  1148. if (controller.value.isPlaying == true) {
  1149. overlay();
  1150. }
  1151. }
  1152. @override
  1153. void dispose() {
  1154. super.dispose();
  1155. timer?.cancel();
  1156. controller.dispose();
  1157. }
  1158. void overlay() {
  1159. if (timer != null) {
  1160. timer!.cancel();
  1161. }
  1162. setState(() {
  1163. opacity = 1;
  1164. });
  1165. timer = Timer(new Duration(seconds: 3), () {
  1166. setState(() {
  1167. if (mounted) {
  1168. if (controller.value.isPlaying == true) opacity = 0;
  1169. }
  1170. });
  1171. });
  1172. }
  1173. void _handleVisibilityChanged(VisibilityInfo info) {
  1174. if (!mounted) return;
  1175. if (controller.value.isInitialized != true) return;
  1176. if (info.visibleFraction == 0) {
  1177. controller.pause();
  1178. setState(() {
  1179. opacity = 1;
  1180. });
  1181. } else {
  1182. // if (controller.value.position.inSeconds > 0) controller.play();
  1183. }
  1184. }
  1185. @override
  1186. Widget build(BuildContext context) {
  1187. return Container(
  1188. width: double.infinity,
  1189. height: double.infinity,
  1190. child: VisibilityDetector(
  1191. key: ValueKey(widget.data.introduceVideo),
  1192. onVisibilityChanged: _handleVisibilityChanged,
  1193. child: Stack(
  1194. fit: StackFit.expand,
  1195. children: [
  1196. VideoPlayer(controller),
  1197. if (!widget.isFullScreen)
  1198. Container(
  1199. width: double.infinity,
  1200. height: double.infinity,
  1201. decoration: BoxDecoration(color: const Color(0xFF040404).withOpacity(.05)),
  1202. ),
  1203. ValueListenableBuilder<VideoPlayerValue>(
  1204. valueListenable: controller,
  1205. builder: (context, v, __) {
  1206. if (v.isInitialized) {
  1207. return GestureDetector(
  1208. behavior: HitTestBehavior.opaque,
  1209. onTap: () {
  1210. if (v.isPlaying) {
  1211. if (opacity >= 1.0) {
  1212. SystemChrome.setPreferredOrientations(Platform.isIOS ? [DeviceOrientation.landscapeRight] : [DeviceOrientation.landscapeLeft]).then((value) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [])).then((value) {
  1213. context.findAncestorStateOfType<_GameDetailPageState>()?.fullScreen(true);
  1214. });
  1215. } else {
  1216. overlay();
  1217. }
  1218. }
  1219. },
  1220. child: AnimatedOpacity(
  1221. duration: new Duration(seconds: 1),
  1222. opacity: opacity,
  1223. child: Container(
  1224. width: double.infinity,
  1225. height: double.infinity,
  1226. child: Stack(
  1227. fit: StackFit.expand,
  1228. children: <Widget>[
  1229. Center(
  1230. child: InkWell(
  1231. child: Image.asset(
  1232. v.isPlaying ? "lib/assets/img/game_icon_suspend.png" : "lib/assets/img/game_icon_play.png",
  1233. height: 50.0,
  1234. ),
  1235. onTap: () {
  1236. if(controller.value.isInitialized) {
  1237. if (v.isPlaying) {
  1238. controller.pause();
  1239. } else {
  1240. if (controller.value.position.compareTo(controller.value.duration) == 0) {
  1241. controller.seekTo(Duration.zero);
  1242. }
  1243. controller.play();
  1244. }
  1245. }else{
  1246. controller.initialize().then((value) => controller.play());
  1247. }
  1248. overlay();
  1249. },
  1250. ),
  1251. ),
  1252. if (controller.value.position.inSeconds > 0)
  1253. Positioned(
  1254. left: 12,
  1255. right: widget.isFullScreen ? 12 : 60,
  1256. bottom: 16,
  1257. child: Row(
  1258. // mainAxisAlignment: MainAxisAlignment.end,
  1259. children: <Widget>[
  1260. Padding(
  1261. padding: EdgeInsets.symmetric(horizontal: 5.0),
  1262. child: Text(DateFormat.toVideoTime(controller.value.position.inSeconds), style: Theme.of(context).textTheme.subtitle1?.copyWith(fontSize: 10.0)),
  1263. ),
  1264. Expanded(
  1265. child: SizedBox(
  1266. height: 3,
  1267. child: VideoProgressIndicator(controller,
  1268. allowScrubbing: true, // 是否可以拖动
  1269. padding: const EdgeInsets.all(0),
  1270. colors: VideoProgressColors(playedColor: Theme.of(context).colorScheme.secondary))),
  1271. ),
  1272. Padding(
  1273. padding: EdgeInsets.symmetric(horizontal: 5.0),
  1274. child: Text(DateFormat.toVideoTime(controller.value.duration.inSeconds), style: Theme.of(context).textTheme.subtitle1?.copyWith(fontSize: 10.0)),
  1275. ),
  1276. ],
  1277. )),
  1278. ],
  1279. ),
  1280. ),
  1281. ),
  1282. );
  1283. }else if(v.isBuffering) {
  1284. return Center(child: CircularProgressIndicator());
  1285. } else {
  1286. return Container(
  1287. width: double.infinity,
  1288. height: double.infinity,
  1289. child: Stack(
  1290. fit: StackFit.expand,
  1291. children: <Widget>[
  1292. CachedNetworkImage(imageUrl: '${widget.data.introduceVideoCover}', fit: BoxFit.fitWidth,),
  1293. Container(
  1294. width: double.infinity,
  1295. height: double.infinity,
  1296. decoration: BoxDecoration(color: const Color(0xFF040404).withOpacity(.05)),
  1297. ),
  1298. Center(
  1299. child: InkWell(
  1300. child: Image.asset(
  1301. "lib/assets/img/game_icon_play.png",
  1302. height: 50.0,
  1303. ),
  1304. onTap: () {
  1305. controller.initialize().then((value) => controller.play());
  1306. overlay();
  1307. },
  1308. ),
  1309. ),
  1310. ],
  1311. ),
  1312. );
  1313. }
  1314. }),
  1315. if (widget.isFullScreen)
  1316. Positioned(
  1317. right: 12,
  1318. top: 12,
  1319. child: InkWell(
  1320. child: Container(
  1321. padding: const EdgeInsets.all(10),
  1322. decoration: const ShapeDecoration(shape: CircleBorder(), color: Colors.black45),
  1323. child: Center(
  1324. child: Image.asset(
  1325. "lib/assets/img/btn_close_white.png",
  1326. height: 22,
  1327. width: 22,
  1328. ),
  1329. ),
  1330. ),
  1331. onTap: () {
  1332. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((value) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top])).then((value) {
  1333. context.findAncestorStateOfType<_GameDetailPageState>()?.fullScreen(false);
  1334. });
  1335. })),
  1336. ],
  1337. ),
  1338. ));
  1339. }
  1340. }