game_board.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:fast_gbk/fast_gbk.dart';
  4. import 'package:file_picker/file_picker.dart';
  5. import 'package:flutter_chinese_chees/utils/game_manager.dart';
  6. import 'package:shirne_dialog/shirne_dialog.dart';
  7. import 'package:flutter/foundation.dart';
  8. import 'package:flutter/material.dart';
  9. import 'package:flutter/services.dart';
  10. import 'package:universal_html/html.dart' as html;
  11. import 'global.dart';
  12. import 'setting.dart';
  13. import 'components/game_bottom_bar.dart';
  14. import 'models/play_mode.dart';
  15. import 'widgets/game_wrapper.dart';
  16. import 'models/game_manager.dart';
  17. import 'components/play.dart';
  18. import 'components/edit_fen.dart';
  19. /// 游戏页面
  20. class GameBoard extends StatefulWidget {
  21. const GameBoard({Key? key}) : super(key: key);
  22. @override
  23. State<GameBoard> createState() => _GameBoardState();
  24. }
  25. class _GameBoardState extends State<GameBoard> {
  26. GameManager gamer = GameManager.instance;
  27. PlayMode? mode;
  28. @override
  29. void initState() {
  30. super.initState();
  31. }
  32. Widget selectMode() {
  33. final maxHeight = MediaQuery.of(context).size.height;
  34. return Center(
  35. child: SizedBox(
  36. height: maxHeight * 0.6,
  37. child: Column(
  38. mainAxisAlignment: MainAxisAlignment.spaceAround,
  39. children: [
  40. ElevatedButton.icon(
  41. onPressed: () {
  42. setState(() {
  43. mode = PlayMode.modeRobot;
  44. });
  45. },
  46. icon: const Icon(Icons.android),
  47. label: Text(context.l10n.modeRobot),
  48. ),
  49. ElevatedButton.icon(
  50. onPressed: () {
  51. MyDialog.toast(
  52. context.l10n.featureNotAvailable,
  53. iconType: IconType.error,
  54. );
  55. },
  56. icon: const Icon(Icons.wifi),
  57. label: Text(context.l10n.modeOnline),
  58. ),
  59. ElevatedButton.icon(
  60. onPressed: () {
  61. setState(() {
  62. mode = PlayMode.modeFree;
  63. });
  64. },
  65. icon: const Icon(Icons.map),
  66. label: Text(context.l10n.modeFree),
  67. ),
  68. if (kIsWeb)
  69. TextButton(
  70. onPressed: () {
  71. var link =
  72. html.window.document.getElementById('download-apk');
  73. if (link == null) {
  74. link = html.window.document.createElement('a');
  75. link.style.display = 'none';
  76. link.setAttribute('id', 'download-apk');
  77. link.setAttribute('target', '_blank');
  78. link.setAttribute('href', 'chinese-chess.apk');
  79. html.window.document
  80. .getElementsByTagName('body')[0]
  81. .append(link);
  82. }
  83. link.click();
  84. },
  85. child: const Text('Download APK'),
  86. ),
  87. ],
  88. ),
  89. ),
  90. );
  91. }
  92. @override
  93. Widget build(BuildContext context) {
  94. return Scaffold(
  95. appBar: AppBar(
  96. title: Text(context.l10n.appTitle),
  97. leading: Builder(
  98. builder: (BuildContext context) {
  99. return IconButton(
  100. icon: const Icon(Icons.menu),
  101. tooltip: context.l10n.openMenu,
  102. onPressed: () {
  103. Scaffold.of(context).openDrawer();
  104. },
  105. );
  106. },
  107. ),
  108. actions: mode == null
  109. ? null
  110. : [
  111. IconButton(
  112. icon: const Icon(Icons.swap_vert),
  113. tooltip: context.l10n.flipBoard,
  114. onPressed: () {
  115. gamer.flip();
  116. },
  117. ),
  118. IconButton(
  119. icon: const Icon(Icons.copy),
  120. tooltip: context.l10n.copyCode,
  121. onPressed: () {
  122. copyFen();
  123. },
  124. ),
  125. IconButton(
  126. icon: const Icon(Icons.airplay),
  127. tooltip: context.l10n.parseCode,
  128. onPressed: () {
  129. applyFen();
  130. },
  131. ),
  132. IconButton(
  133. icon: const Icon(Icons.airplay),
  134. tooltip: context.l10n.editCode,
  135. onPressed: () {
  136. editFen();
  137. },
  138. ),
  139. /*IconButton(icon: Icon(Icons.minimize), onPressed: (){
  140. }),
  141. IconButton(icon: Icon(Icons.zoom_out_map), onPressed: (){
  142. }),
  143. IconButton(icon: Icon(Icons.clear), color: Colors.red, onPressed: (){
  144. this._showDialog(context.l10n.exit_now,
  145. [
  146. TextButton(
  147. onPressed: (){
  148. Navigator.of(context).pop();
  149. },
  150. child: Text(context.l10n.dont_exit),
  151. ),
  152. TextButton(
  153. onPressed: (){
  154. if(!kIsWeb){
  155. Isolate.current.pause();
  156. exit(0);
  157. }
  158. },
  159. child: Text(context.l10n.yes_exit,style: TextStyle(color:Colors.red)),
  160. )
  161. ]
  162. );
  163. })*/
  164. ],
  165. ),
  166. drawer: Drawer(
  167. semanticLabel: context.l10n.menu,
  168. child: ListView(
  169. padding: EdgeInsets.zero,
  170. children: [
  171. DrawerHeader(
  172. decoration: const BoxDecoration(
  173. color: Colors.blue,
  174. ),
  175. child: Center(
  176. child: Column(
  177. children: [
  178. Image.asset(
  179. 'assets/images/logo.png',
  180. width: 100,
  181. height: 100,
  182. ),
  183. Text(
  184. context.l10n.appTitle,
  185. style: const TextStyle(
  186. color: Colors.white,
  187. fontSize: 24,
  188. ),
  189. ),
  190. ],
  191. ),
  192. ),
  193. ),
  194. ListTile(
  195. leading: const Icon(Icons.add),
  196. title: Text(context.l10n.newGame),
  197. onTap: () {
  198. Navigator.pop(context);
  199. setState(() {
  200. if (mode == null) {
  201. setState(() {
  202. mode = PlayMode.modeFree;
  203. });
  204. }
  205. gamer.newGame();
  206. //mode = null;
  207. });
  208. },
  209. ),
  210. ListTile(
  211. leading: const Icon(Icons.description),
  212. title: Text(context.l10n.loadManual),
  213. onTap: () {
  214. Navigator.pop(context);
  215. if (mode == null) {
  216. setState(() {
  217. mode = PlayMode.modeFree;
  218. });
  219. }
  220. loadFile();
  221. },
  222. ),
  223. ListTile(
  224. leading: const Icon(Icons.save),
  225. title: Text(context.l10n.saveManual),
  226. onTap: () {
  227. Navigator.pop(context);
  228. saveManual();
  229. },
  230. ),
  231. ListTile(
  232. leading: const Icon(Icons.copy),
  233. title: Text(context.l10n.copyCode),
  234. onTap: () {
  235. Navigator.pop(context);
  236. copyFen();
  237. },
  238. ),
  239. ListTile(
  240. leading: const Icon(Icons.settings),
  241. title: Text(context.l10n.setting),
  242. onTap: () {
  243. Navigator.pop(context);
  244. Navigator.push(
  245. context,
  246. MaterialPageRoute(
  247. builder: (BuildContext context) => const SettingPage(),
  248. ),
  249. );
  250. },
  251. ),
  252. ],
  253. ),
  254. ),
  255. body: SafeArea(
  256. child: Center(
  257. child: mode == null ? selectMode() : PlayPage(mode: mode!),
  258. ),
  259. ),
  260. bottomNavigationBar:
  261. (mode == null || MediaQuery.of(context).size.width >= 980)
  262. ? null
  263. : GameBottomBar(mode!),
  264. );
  265. }
  266. void editFen() {
  267. Navigator.of(context).push<String>(
  268. MaterialPageRoute(
  269. builder: (BuildContext context) {
  270. return GameWrapper(child: EditFen(fen: gamer.fenStr));
  271. },
  272. ),
  273. ).then((fenStr) {
  274. if (fenStr != null && fenStr.isNotEmpty) {
  275. gamer.newGame(fenStr);
  276. }
  277. });
  278. }
  279. Future<void> applyFen() async {
  280. final l10n = context.l10n;
  281. ClipboardData? cData = await Clipboard.getData(Clipboard.kTextPlain);
  282. String fenStr = cData?.text ?? '';
  283. TextEditingController filenameController =
  284. TextEditingController(text: fenStr);
  285. filenameController.addListener(() {
  286. fenStr = filenameController.text;
  287. });
  288. final confirmed = await MyDialog.confirm(
  289. TextField(
  290. controller: filenameController,
  291. ),
  292. buttonText: l10n.apply,
  293. title: l10n.situationCode,
  294. );
  295. if (confirmed ?? false) {
  296. if (RegExp(
  297. r'^[abcnrkpABCNRKP\d]{1,9}(?:/[abcnrkpABCNRKP\d]{1,9}){9}(\s[wb]\s-\s-\s\d+\s\d+)?$',
  298. ).hasMatch(fenStr)) {
  299. gamer.newGame(fenStr);
  300. } else {
  301. MyDialog.alert(l10n.invalidCode);
  302. }
  303. }
  304. }
  305. void copyFen() {
  306. Clipboard.setData(ClipboardData(text: gamer.fenStr));
  307. MyDialog.alert(context.l10n.copySuccess);
  308. }
  309. Future<void> saveManual() async {
  310. String content = gamer.manual.export();
  311. String filename = '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.pgn';
  312. if (kIsWeb) {
  313. await _saveManualWeb(content, filename);
  314. } else if (Platform.isAndroid || Platform.isIOS) {
  315. await _saveManualNative(content, filename);
  316. }
  317. }
  318. Future<void> _saveManualNative(String content, String filename) async {
  319. final result = await FilePicker.platform.saveFile(
  320. dialogTitle: 'Save pgn file',
  321. fileName: filename,
  322. allowedExtensions: ['pgn'],
  323. );
  324. if (context.mounted && result != null) {
  325. List<int> fData = gbk.encode(content);
  326. await File('$result/$filename').writeAsBytes(fData);
  327. MyDialog.toast(context.l10n.saveSuccess);
  328. }
  329. }
  330. Future<void> _saveManualWeb(String content, String filename) async {
  331. List<int> fData = gbk.encode(content);
  332. var link = html.window.document.createElement('a');
  333. link.setAttribute('download', filename);
  334. link.style.display = 'none';
  335. link.setAttribute('href', Uri.dataFromBytes(fData).toString());
  336. html.window.document.getElementsByTagName('body')[0].append(link);
  337. link.click();
  338. await Future<void>.delayed(const Duration(seconds: 10));
  339. link.remove();
  340. }
  341. Future<void> loadFile() async {
  342. FilePickerResult? result = await FilePicker.platform.pickFiles(
  343. type: FileType.custom,
  344. allowedExtensions: ['pgn', 'PGN'],
  345. withData: true,
  346. );
  347. if (result != null && result.count == 1) {
  348. String content = gbk.decode(result.files.single.bytes!);
  349. if (gamer.isStop) {
  350. gamer.newGame();
  351. }
  352. gamer.loadPGN(content);
  353. } else {
  354. // User canceled the picker
  355. }
  356. }
  357. }