Browse Source

Automatic Commit By liuyuqi

liuyuqi-dellpc 3 years ago
commit
b471b752a0
100 changed files with 3939 additions and 0 deletions
  1. 73 0
      .gitignore
  2. 10 0
      .metadata
  3. 68 0
      LICENSE
  4. 37 0
      README.md
  5. 33 0
      README_EN.md
  6. 7 0
      android/.gitignore
  7. 67 0
      android/app/build.gradle
  8. 7 0
      android/app/src/debug/AndroidManifest.xml
  9. 30 0
      android/app/src/main/AndroidManifest.xml
  10. BIN
      android/app/src/main/ic_launcher-web.png
  11. 12 0
      android/app/src/main/kotlin/tech/soit/flutter_tetris/MainActivity.kt
  12. 12 0
      android/app/src/main/res/drawable/launch_background.xml
  13. 5 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  14. 5 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  15. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  16. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  17. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  18. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  19. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  20. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  21. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  22. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  23. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  24. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  25. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  26. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  27. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  28. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  29. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  30. 4 0
      android/app/src/main/res/values/ic_launcher_background.xml
  31. 8 0
      android/app/src/main/res/values/styles.xml
  32. 7 0
      android/app/src/profile/AndroidManifest.xml
  33. 31 0
      android/build.gradle
  34. 4 0
      android/gradle.properties
  35. 6 0
      android/gradle/wrapper/gradle-wrapper.properties
  36. 15 0
      android/settings.gradle
  37. BIN
      assets/audios/clean.mp3
  38. BIN
      assets/audios/drop.mp3
  39. BIN
      assets/audios/explosion.mp3
  40. BIN
      assets/audios/move.mp3
  41. BIN
      assets/audios/rotate.mp3
  42. BIN
      assets/audios/start.mp3
  43. BIN
      assets/material.png
  44. 32 0
      ios/.gitignore
  45. 26 0
      ios/Flutter/AppFrameworkInfo.plist
  46. 2 0
      ios/Flutter/Debug.xcconfig
  47. 2 0
      ios/Flutter/Release.xcconfig
  48. 90 0
      ios/Podfile
  49. 584 0
      ios/Runner.xcodeproj/project.pbxproj
  50. 7 0
      ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  51. 91 0
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  52. 10 0
      ios/Runner.xcworkspace/contents.xcworkspacedata
  53. 13 0
      ios/Runner/AppDelegate.swift
  54. 122 0
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  55. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  56. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  57. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  58. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  59. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  60. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  61. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  62. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  63. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  64. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  65. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  66. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  67. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  68. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  69. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  70. 23 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  71. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  72. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  73. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  74. 5 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  75. 37 0
      ios/Runner/Base.lproj/LaunchScreen.storyboard
  76. 26 0
      ios/Runner/Base.lproj/Main.storyboard
  77. 45 0
      ios/Runner/Info.plist
  78. 1 0
      ios/Runner/Runner-Bridging-Header.h
  79. 154 0
      lib/gamer/block.dart
  80. 433 0
      lib/gamer/gamer.dart
  81. 60 0
      lib/gamer/keyboard.dart
  82. 130 0
      lib/generated/i18n.dart
  83. 118 0
      lib/income/donation_dialog.dart
  84. 45 0
      lib/main.dart
  85. 88 0
      lib/material/audios.dart
  86. 59 0
      lib/material/briks.dart
  87. 250 0
      lib/material/images.dart
  88. 48 0
      lib/material/material.dart
  89. 3 0
      lib/model/config.dart
  90. 13 0
      lib/pages/home_page.dart
  91. 368 0
      lib/panel/controller.dart
  92. 58 0
      lib/panel/page_land.dart
  93. 77 0
      lib/panel/page_portrait.dart
  94. 81 0
      lib/panel/player_panel.dart
  95. 108 0
      lib/panel/screen.dart
  96. 116 0
      lib/panel/status_panel.dart
  97. 18 0
      lib/utils/app_util.dart
  98. 1 0
      linux/.gitignore
  99. 143 0
      linux/Makefile
  100. 11 0
      linux/flutter/generated_plugin_registrant.cc

+ 73 - 0
.gitignore

@@ -0,0 +1,73 @@
+# Miscellaneous
+*.class
+*.lock
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Visual Studio Code related
+.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+lib/generated_plugin_registrant.dart
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

+ 10 - 0
.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b
+  channel: beta
+
+project_type: app

+ 68 - 0
LICENSE

@@ -0,0 +1,68 @@
+MIT License
+
+Copyright (c) 2019 YangBin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
+996 License Version 1.0 (Draft)
+
+Permission is hereby granted to any individual or legal entity
+obtaining a copy of this licensed work (including the source code,
+documentation and/or related items, hereinafter collectively referred
+to as the "licensed work"), free of charge, to deal with the licensed
+work for any purpose, including without limitation, the rights to use,
+reproduce, modify, prepare derivative works of, distribute, publish
+and sublicense the licensed work, subject to the following conditions:
+
+1. The individual or the legal entity must conspicuously display,
+without modification, this License and the notice on each redistributed
+or derivative copy of the Licensed Work.
+
+2. The individual or the legal entity must strictly comply with all
+applicable laws, regulations, rules and standards of the jurisdiction
+relating to labor and employment where the individual is physically
+located or where the individual was born or naturalized; or where the
+legal entity is registered or is operating (whichever is stricter). In
+case that the jurisdiction has no such laws, regulations, rules and
+standards or its laws, regulations, rules and standards are
+unenforceable, the individual or the legal entity are required to
+comply with Core International Labor Standards.
+
+3. The individual or the legal entity shall not induce or force its
+employee(s), whether full-time or part-time, or its independent
+contractor(s), in any methods, to agree in oral or written form, to
+directly or indirectly restrict, weaken or relinquish his or her
+rights or remedies under such laws, regulations, rules and standards
+relating to labor and employment as mentioned above, no matter whether
+such written or oral agreement are enforceable under the laws of the
+said jurisdiction, nor shall such individual or the legal entity
+limit, in any methods, the rights of its employee(s) or independent
+contractor(s) from reporting or complaining to the copyright holder or
+relevant authorities monitoring the compliance of the license about
+its violation(s) of the said license.
+
+THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE
+LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.

+ 37 - 0
README.md

@@ -0,0 +1,37 @@
+#### English introduction
+
+Please view [README_EN](https://github.com/boyan01/flutter-tetris/blob/master/README_EN.md)
+
+# Flutter俄罗斯方块
+<a href="https://github.com/Solido/awesome-flutter"><img alt="Awesome Flutter" src="https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square" /></a> [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) [![LICENSE](https://img.shields.io/badge/license-NPL%20(The%20996%20Prohibited%20License)-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE)
+
+---
+
+使用Flutter开发的俄罗斯方块游戏。支持 **Android**, **iOS**, **Windows**, **mac**, **Linux** 以及 **web**.
+
+参考来源于 [vue-tetris](https://github.com/Binaryify/vue-tetris) 。
+
+## 如何开始
+
+* 自行编译
+
+  安装[Flutter](https://flutter.io/docs/get-started/install)
+
+  在命令行输入:`flutter run --profile`
+
+* 前往[Release](https://github.com/boyan01/flutter-tetris/releases) 下载 Apk/macOS/Windows 可执行文件。
+
+* 或者前往 [https://boyan01.github.io/flutter-tetris](https://boyan01.github.io/flutter-tetris/#/) 体验 Web 版本
+
+## 效果预览
+
+![效果预览](./_preview/game_gif.gif)
+
+支持横屏模式
+
+![横屏](./_preview/screen_land.jpg)
+
+
+## 其他
+
+MIT with 996 License

+ 33 - 0
README_EN.md

@@ -0,0 +1,33 @@
+#### 中文介绍
+
+请前往 [README](https://github.com/boyan01/flutter-tetris/blob/master/README.md)
+# flutter-tetris
+<a href="https://github.com/Solido/awesome-flutter"><img alt="Awesome Flutter" src="https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square" /></a> [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) [![LICENSE](https://img.shields.io/badge/license-NPL%20(The%20996%20Prohibited%20License)-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE)
+
+---
+
+a tetris game powered by flutter.
+
+Inspired by [vue-tetris](https://github.com/Binaryify/vue-tetris).
+
+## Getting Started
+
+* Compile by yourself
+
+  install [Flutter](https://flutter.io/docs/get-started/install)
+
+  run in Command Line:`flutter run --profile`
+
+* download Apk file at page [releases](https://github.com/boyan01/flutter-tetris/releases)(for Android only)
+
+## Preview
+
+![preview](./_preview/game_gif.gif)
+
+support landscape
+
+![land](./_preview/screen_land.jpg)
+
+## Other
+
+MIT with 996 License

+ 7 - 0
android/.gitignore

@@ -0,0 +1,7 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java

+ 67 - 0
android/app/build.gradle

@@ -0,0 +1,67 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 30
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "tech.soit.flutter_tetris"
+        minSdkVersion 21
+        targetSdkVersion 30
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig signingConfigs.debug
+        }
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test:runner:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+}

+ 7 - 0
android/app/src/debug/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="tech.soit.flutter_tetris">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 30 - 0
android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="tech.soit.flutter_tetris">
+    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+         calls FlutterMain.startInitialization(this); in its onCreate method.
+         In most cases you can leave this as-is, but you if you want to provide
+         additional functionality it is fine to subclass or reimplement
+         FlutterApplication and put your custom class here. -->
+    <application
+        android:name="io.flutter.app.FlutterApplication"
+        android:label="flutter_tetris"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>

BIN
android/app/src/main/ic_launcher-web.png


+ 12 - 0
android/app/src/main/kotlin/tech/soit/flutter_tetris/MainActivity.kt

@@ -0,0 +1,12 @@
+package tech.soit.flutter_tetris
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainActivity: FlutterActivity() {
+    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+        GeneratedPluginRegistrant.registerWith(flutterEngine);
+    }
+}

+ 12 - 0
android/app/src/main/res/drawable/launch_background.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>

+ 5 - 0
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

+ 5 - 0
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 4 - 0
android/app/src/main/res/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#26A69A</color>
+</resources>

+ 8 - 0
android/app/src/main/res/values/styles.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+</resources>

+ 7 - 0
android/app/src/profile/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="tech.soit.flutter_tetris">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 31 - 0
android/build.gradle

@@ -0,0 +1,31 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 4 - 0
android/gradle.properties

@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true

+ 6 - 0
android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

+ 15 - 0
android/settings.gradle

@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}

BIN
assets/audios/clean.mp3


BIN
assets/audios/drop.mp3


BIN
assets/audios/explosion.mp3


BIN
assets/audios/move.mp3


BIN
assets/audios/rotate.mp3


BIN
assets/audios/start.mp3


BIN
assets/material.png


+ 32 - 0
ios/.gitignore

@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3

+ 26 - 0
ios/Flutter/AppFrameworkInfo.plist

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>CFBundleDevelopmentRegion</key>
+  <string>$(DEVELOPMENT_LANGUAGE)</string>
+  <key>CFBundleExecutable</key>
+  <string>App</string>
+  <key>CFBundleIdentifier</key>
+  <string>io.flutter.flutter.app</string>
+  <key>CFBundleInfoDictionaryVersion</key>
+  <string>6.0</string>
+  <key>CFBundleName</key>
+  <string>App</string>
+  <key>CFBundlePackageType</key>
+  <string>FMWK</string>
+  <key>CFBundleShortVersionString</key>
+  <string>1.0</string>
+  <key>CFBundleSignature</key>
+  <string>????</string>
+  <key>CFBundleVersion</key>
+  <string>1.0</string>
+  <key>MinimumOSVersion</key>
+  <string>8.0</string>
+</dict>
+</plist>

+ 2 - 0
ios/Flutter/Debug.xcconfig

@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"

+ 2 - 0
ios/Flutter/Release.xcconfig

@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"

+ 90 - 0
ios/Podfile

@@ -0,0 +1,90 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def parse_KV_file(file, separator='=')
+  file_abs_path = File.expand_path(file)
+  if !File.exists? file_abs_path
+    return [];
+  end
+  generated_key_values = {}
+  skip_line_start_symbols = ["#", "/"]
+  File.foreach(file_abs_path) do |line|
+    next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
+    plugin = line.split(pattern=separator)
+    if plugin.length == 2
+      podname = plugin[0].strip()
+      path = plugin[1].strip()
+      podpath = File.expand_path("#{path}", file_abs_path)
+      generated_key_values[podname] = podpath
+    else
+      puts "Invalid plugin specification: #{line}"
+    end
+  end
+  generated_key_values
+end
+
+target 'Runner' do
+  use_frameworks!
+  use_modular_headers!
+
+  # Flutter Pod
+
+  copied_flutter_dir = File.join(__dir__, 'Flutter')
+  copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
+  copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
+  unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
+    # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
+    # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
+    # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
+
+    generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
+    unless File.exist?(generated_xcode_build_settings_path)
+      raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+    end
+    generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
+    cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
+
+    unless File.exist?(copied_framework_path)
+      FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
+    end
+    unless File.exist?(copied_podspec_path)
+      FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
+    end
+  end
+
+  # Keep pod path relative so it can be checked into Podfile.lock.
+  pod 'Flutter', :path => 'Flutter'
+
+  # Plugin Pods
+
+  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
+  # referring to absolute paths on developers' machines.
+  system('rm -rf .symlinks')
+  system('mkdir -p .symlinks/plugins')
+  plugin_pods = parse_KV_file('../.flutter-plugins')
+  plugin_pods.each do |name, path|
+    symlink = File.join('.symlinks', 'plugins', name)
+    File.symlink(path, symlink)
+    pod name, :path => File.join(symlink, 'ios')
+  end
+end
+
+# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
+install! 'cocoapods', :disable_input_output_paths => true
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    target.build_configurations.each do |config|
+      config.build_settings['ENABLE_BITCODE'] = 'NO'
+    end
+  end
+end

+ 584 - 0
ios/Runner.xcodeproj/project.pbxproj

@@ -0,0 +1,584 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
+		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		5C0CE568E15E98E07D116769 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A61363D6699EDC5B3E8CF46 /* Pods_Runner.framework */; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
+		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
+				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+		354D2140C348863997D61F09 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
+		521300D6E9C3A09991429CD2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
+		742DD36AAD6926A795A797C2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+		8A61363D6699EDC5B3E8CF46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
+				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
+				5C0CE568E15E98E07D116769 /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		10C7F7AA9FF17201C63670BF /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				742DD36AAD6926A795A797C2 /* Pods-Runner.debug.xcconfig */,
+				354D2140C348863997D61F09 /* Pods-Runner.release.xcconfig */,
+				521300D6E9C3A09991429CD2 /* Pods-Runner.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B80C3931E831B6300D905FE /* App.framework */,
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEBA1CF902C7004384FC /* Flutter.framework */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+				10C7F7AA9FF17201C63670BF /* Pods */,
+				DB3CC3345158B8C6AA46BEF5 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				97C146F11CF9000F007C117D /* Supporting Files */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		97C146F11CF9000F007C117D /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		DB3CC3345158B8C6AA46BEF5 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				8A61363D6699EDC5B3E8CF46 /* Pods_Runner.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				B804686302B84009417001E6 /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+				8D305A54E8A98A1D867EA98C /* [CP] Embed Pods Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		97C146E61CF9000F007C117D /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 1020;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						LastSwiftMigration = 1100;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Thin Binary";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+		};
+		8D305A54E8A98A1D867EA98C /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		9740EEB61CF901F6004384FC /* Run Script */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Run Script";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+		};
+		B804686302B84009417001E6 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C146FB1CF9000F007C117D /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C147001CF9000F007C117D /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = tech.soit.flutterTetris;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = tech.soit.flutterTetris;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = tech.soit.flutterTetris;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}

+ 7 - 0
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>

+ 91 - 0
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+               BuildableName = "Runner.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 10 - 0
ios/Runner.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>

+ 13 - 0
ios/Runner/AppDelegate.swift

@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+  override func application(
+    _ application: UIApplication,
+    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+  ) -> Bool {
+    GeneratedPluginRegistrant.register(with: self)
+    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+  }
+}

+ 122 - 0
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,122 @@
+{
+  "images" : [
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-20x20@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-29x29@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-40x40@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-20x20@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-29x29@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-40x40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-76x76@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "83.5x83.5",
+      "idiom" : "ipad",
+      "filename" : "Icon-App-83.5x83.5@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "1024x1024",
+      "idiom" : "ios-marketing",
+      "filename" : "Icon-App-1024x1024@1x.png",
+      "scale" : "1x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png


BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png


+ 23 - 0
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png


BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png


BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png


+ 5 - 0
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md

@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

+ 37 - 0
ios/Runner/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+                            </imageView>
+                        </subviews>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+                        </constraints>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+    <resources>
+        <image name="LaunchImage" width="168" height="185"/>
+    </resources>
+</document>

+ 26 - 0
ios/Runner/Base.lproj/Main.storyboard

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--Flutter View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+</document>

+ 45 - 0
ios/Runner/Info.plist

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>flutter_tetris</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UIViewControllerBasedStatusBarAppearance</key>
+	<false/>
+</dict>
+</plist>

+ 1 - 0
ios/Runner/Runner-Bridging-Header.h

@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"

+ 154 - 0
lib/gamer/block.dart

@@ -0,0 +1,154 @@
+import 'gamer.dart';
+import 'dart:math' as math;
+
+const BLOCK_SHAPES = {
+  BlockType.I: [
+    [1, 1, 1, 1]
+  ],
+  BlockType.L: [
+    [0, 0, 1],
+    [1, 1, 1],
+  ],
+  BlockType.J: [
+    [1, 0, 0],
+    [1, 1, 1],
+  ],
+  BlockType.Z: [
+    [1, 1, 0],
+    [0, 1, 1],
+  ],
+  BlockType.S: [
+    [0, 1, 1],
+    [1, 1, 0],
+  ],
+  BlockType.O: [
+    [1, 1],
+    [1, 1]
+  ],
+  BlockType.T: [
+    [0, 1, 0],
+    [1, 1, 1]
+  ]
+};
+
+///方块初始化时的位置
+const START_XY = {
+  BlockType.I: [3, 0],
+  BlockType.L: [4, -1],
+  BlockType.J: [4, -1],
+  BlockType.Z: [4, -1],
+  BlockType.S: [4, -1],
+  BlockType.O: [4, -1],
+  BlockType.T: [4, -1],
+};
+
+///方块变换时的中心点
+const ORIGIN = {
+  BlockType.I: [
+    [1, -1],
+    [-1, 1],
+  ],
+  BlockType.L: [
+    [0, 0]
+  ],
+  BlockType.J: [
+    [0, 0]
+  ],
+  BlockType.Z: [
+    [0, 0]
+  ],
+  BlockType.S: [
+    [0, 0]
+  ],
+  BlockType.O: [
+    [0, 0]
+  ],
+  BlockType.T: [
+    [0, 0],
+    [0, 1],
+    [1, -1],
+    [-1, 0]
+  ],
+};
+
+enum BlockType { I, L, J, Z, S, O, T }
+
+class Block {
+  final BlockType type;
+  final List<List<int>> shape;
+  final List<int> xy;
+  final int rotateIndex;
+
+  Block(this.type, this.shape, this.xy, this.rotateIndex);
+
+  Block fall({int step = 1}) {
+    return Block(type, shape, [xy[0], xy[1] + step], rotateIndex);
+  }
+
+  Block right() {
+    return Block(type, shape, [xy[0] + 1, xy[1]], rotateIndex);
+  }
+
+  Block left() {
+    return Block(type, shape, [xy[0] - 1, xy[1]], rotateIndex);
+  }
+
+  Block rotate() {
+    List<List<int>> result =
+        List.filled(shape[0].length, null, growable: false);
+    for (int row = 0; row < shape.length; row++) {
+      for (int col = 0; col < shape[row].length; col++) {
+        if (result[col] == null) {
+          result[col] = List.filled(shape.length, 0, growable: false);
+        }
+        result[col][row] = shape[shape.length - 1 - row][col];
+      }
+    }
+    final nextXy = [
+      this.xy[0] + ORIGIN[type][rotateIndex][0],
+      this.xy[1] + ORIGIN[type][rotateIndex][1]
+    ];
+    final nextRotateIndex =
+        rotateIndex + 1 >= ORIGIN[this.type].length ? 0 : rotateIndex + 1;
+
+    return Block(type, result, nextXy, nextRotateIndex);
+  }
+
+  bool isValidInMatrix(List<List<int>> matrix) {
+    if (xy[1] + shape.length > GAME_PAD_MATRIX_H ||
+        xy[0] < 0 ||
+        xy[0] + shape[0].length > GAME_PAD_MATRIX_W) {
+      return false;
+    }
+    for (var i = 0; i < matrix.length; i++) {
+      final line = matrix[i];
+      for (var j = 0; j < line.length; j++) {
+        if (line[j] == 1 && get(j, i) == 1) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  ///return null if do not show at [x][y]
+  ///return 1 if show at [x,y]
+  int get(int x, int y) {
+    x -= xy[0];
+    y -= xy[1];
+    if (x < 0 || x >= shape[0].length || y < 0 || y >= shape.length) {
+      return null;
+    }
+    return shape[y][x] == 1 ? 1 : null;
+  }
+
+  static Block fromType(BlockType type) {
+    final shape = BLOCK_SHAPES[type];
+    return Block(type, shape, START_XY[type], 0);
+  }
+
+  static Block getRandom() {
+    final i = math.Random().nextInt(BlockType.values.length);
+    return fromType(BlockType.values[i]);
+  }
+}

+ 433 - 0
lib/gamer/gamer.dart

@@ -0,0 +1,433 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:tetris/gamer/block.dart';
+import 'package:tetris/main.dart';
+import 'package:tetris/material/audios.dart';
+
+///the height of game pad
+const GAME_PAD_MATRIX_H = 20;
+
+///the width of game pad
+const GAME_PAD_MATRIX_W = 10;
+
+///state of [GameControl]
+enum GameStates {
+  ///随时可以开启一把惊险而又刺激的俄罗斯方块
+  none,
+
+  ///游戏暂停中,方块的下落将会停止
+  paused,
+
+  ///游戏正在进行中,方块正在下落
+  ///按键可交互
+  running,
+
+  ///游戏正在重置
+  ///重置完成之后,[GameController]状态将会迁移为[none]
+  reset,
+
+  ///下落方块已经到达底部,此时正在将方块固定在游戏矩阵中
+  ///固定完成之后,将会立即开始下一个方块的下落任务
+  mixing,
+
+  ///正在消除行
+  ///消除完成之后,将会立刻开始下一个方块的下落任务
+  clear,
+
+  ///方块快速下坠到底部
+  drop,
+}
+
+class Game extends StatefulWidget {
+  final Widget child;
+
+  const Game({Key key, @required this.child})
+      : assert(child != null),
+        super(key: key);
+
+  @override
+  State<StatefulWidget> createState() {
+    return GameControl();
+  }
+
+  static GameControl of(BuildContext context) {
+    final state = context.ancestorStateOfType(TypeMatcher<GameControl>());
+    assert(state != null, "must wrap this context with [Game]");
+    return state;
+  }
+}
+
+///duration for show a line when reset
+const _REST_LINE_DURATION = const Duration(milliseconds: 50);
+
+const _LEVEL_MAX = 6;
+
+const _LEVEL_MIN = 1;
+
+const _SPEED = [
+  const Duration(milliseconds: 800),
+  const Duration(milliseconds: 650),
+  const Duration(milliseconds: 500),
+  const Duration(milliseconds: 370),
+  const Duration(milliseconds: 250),
+  const Duration(milliseconds: 160),
+];
+
+class GameControl extends State<Game> with RouteAware {
+  GameControl() {
+    //inflate game pad data
+    for (int i = 0; i < GAME_PAD_MATRIX_H; i++) {
+      _data.add(List.filled(GAME_PAD_MATRIX_W, 0));
+      _mask.add(List.filled(GAME_PAD_MATRIX_W, 0));
+    }
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    routeObserver.subscribe(this, ModalRoute.of(context));
+  }
+
+  @override
+  void dispose() {
+    routeObserver.unsubscribe(this);
+    super.dispose();
+  }
+
+  @override
+  void didPushNext() {
+    //pause when screen is at background
+    pause();
+  }
+
+  ///the gamer data
+  final List<List<int>> _data = [];
+
+  ///在 [build] 方法中于 [_data]混合,形成一个新的矩阵
+  ///[_mask]矩阵的宽高与 [_data] 一致
+  ///对于任意的 _mask[x,y] :
+  /// 如果值为 0,则对 [_data]没有任何影响
+  /// 如果值为 -1,则表示 [_data] 中该行不显示
+  /// 如果值为 1,则表示 [_data] 中该行高亮
+  final List<List<int>> _mask = [];
+
+  ///from 1-6
+  int _level = 1;
+
+  int _points = 0;
+
+  int _cleared = 0;
+
+  Block _current;
+
+  Block _next = Block.getRandom();
+
+  GameStates _states = GameStates.none;
+
+  Block _getNext() {
+    final next = _next;
+    _next = Block.getRandom();
+    return next;
+  }
+
+  SoundState get _sound => Sound.of(context);
+
+  void rotate() {
+    if (_states == GameStates.running && _current != null) {
+      final next = _current.rotate();
+      if (next.isValidInMatrix(_data)) {
+        _current = next;
+        _sound.rotate();
+      }
+    }
+    setState(() {});
+  }
+
+  void right() {
+    if (_states == GameStates.none && _level < _LEVEL_MAX) {
+      _level++;
+    } else if (_states == GameStates.running && _current != null) {
+      final next = _current.right();
+      if (next.isValidInMatrix(_data)) {
+        _current = next;
+        _sound.move();
+      }
+    }
+    setState(() {});
+  }
+
+  void left() {
+    if (_states == GameStates.none && _level > _LEVEL_MIN) {
+      _level--;
+    } else if (_states == GameStates.running && _current != null) {
+      final next = _current.left();
+      if (next.isValidInMatrix(_data)) {
+        _current = next;
+        _sound.move();
+      }
+    }
+    setState(() {});
+  }
+
+  void drop() async {
+    if (_states == GameStates.running && _current != null) {
+      for (int i = 0; i < GAME_PAD_MATRIX_H; i++) {
+        final fall = _current.fall(step: i + 1);
+        if (!fall.isValidInMatrix(_data)) {
+          _current = _current.fall(step: i);
+          _states = GameStates.drop;
+          setState(() {});
+          await Future.delayed(const Duration(milliseconds: 100));
+          _mixCurrentIntoData(mixSound: _sound.fall);
+          break;
+        }
+      }
+      setState(() {});
+    } else if (_states == GameStates.paused || _states == GameStates.none) {
+      _startGame();
+    }
+  }
+
+  void down({bool enableSounds = true}) {
+    if (_states == GameStates.running && _current != null) {
+      final next = _current.fall();
+      if (next.isValidInMatrix(_data)) {
+        _current = next;
+        if (enableSounds) {
+          _sound.move();
+        }
+      } else {
+        _mixCurrentIntoData();
+      }
+    }
+    setState(() {});
+  }
+
+  Timer _autoFallTimer;
+
+  ///mix current into [_data]
+  Future<void> _mixCurrentIntoData({void mixSound()}) async {
+    if (_current == null) {
+      return;
+    }
+    //cancel the auto falling task
+    _autoFall(false);
+
+    _forTable((i, j) => _data[i][j] = _current.get(j, i) ?? _data[i][j]);
+
+    //消除行
+    final clearLines = [];
+    for (int i = 0; i < GAME_PAD_MATRIX_H; i++) {
+      if (_data[i].every((d) => d == 1)) {
+        clearLines.add(i);
+      }
+    }
+
+    if (clearLines.isNotEmpty) {
+      setState(() => _states = GameStates.clear);
+
+      _sound.clear();
+
+      ///消除效果动画
+      for (int count = 0; count < 5; count++) {
+        clearLines.forEach((line) {
+          _mask[line].fillRange(0, GAME_PAD_MATRIX_W, count % 2 == 0 ? -1 : 1);
+        });
+        setState(() {});
+        await Future.delayed(Duration(milliseconds: 100));
+      }
+      clearLines
+          .forEach((line) => _mask[line].fillRange(0, GAME_PAD_MATRIX_W, 0));
+
+      //移除所有被消除的行
+      clearLines.forEach((line) {
+        _data.setRange(1, line + 1, _data);
+        _data[0] = List.filled(GAME_PAD_MATRIX_W, 0);
+      });
+      debugPrint("clear lines : $clearLines");
+
+      _cleared += clearLines.length;
+      _points += clearLines.length * _level * 5;
+
+      //up level possible when cleared
+      int level = (_cleared ~/ 50) + _LEVEL_MIN;
+      _level = level <= _LEVEL_MAX && level > _level ? level : _level;
+    } else {
+      _states = GameStates.mixing;
+      if (mixSound != null) mixSound();
+      _forTable((i, j) => _mask[i][j] = _current.get(j, i) ?? _mask[i][j]);
+      setState(() {});
+      await Future.delayed(const Duration(milliseconds: 200));
+      _forTable((i, j) => _mask[i][j] = 0);
+      setState(() {});
+    }
+
+    //_current已经融入_data了,所以不再需要
+    _current = null;
+
+    //检查游戏是否结束,即检查第一行是否有元素为1
+    if (_data[0].contains(1)) {
+      reset();
+      return;
+    } else {
+      //游戏尚未结束,开启下一轮方块下落
+      _startGame();
+    }
+  }
+
+  ///遍历表格
+  ///i 为 row
+  ///j 为 column
+  static void _forTable(dynamic function(int row, int column)) {
+    for (int i = 0; i < GAME_PAD_MATRIX_H; i++) {
+      for (int j = 0; j < GAME_PAD_MATRIX_W; j++) {
+        final b = function(i, j);
+        if (b is bool && b) {
+          break;
+        }
+      }
+    }
+  }
+
+  void _autoFall(bool enable) {
+    if (!enable && _autoFallTimer != null) {
+      _autoFallTimer.cancel();
+      _autoFallTimer = null;
+    } else if (enable) {
+      _autoFallTimer?.cancel();
+      _current = _current ?? _getNext();
+      _autoFallTimer = Timer.periodic(_SPEED[_level - 1], (t) {
+        down(enableSounds: false);
+      });
+    }
+  }
+
+  void pause() {
+    if (_states == GameStates.running) {
+      _states = GameStates.paused;
+    }
+    setState(() {});
+  }
+
+  void pauseOrResume() {
+    if (_states == GameStates.running) {
+      pause();
+    } else if (_states == GameStates.paused || _states == GameStates.none) {
+      _startGame();
+    }
+  }
+
+  void reset() {
+    if (_states == GameStates.none) {
+      //可以开始游戏
+      _startGame();
+      return;
+    }
+    if (_states == GameStates.reset) {
+      return;
+    }
+    _sound.start();
+    _states = GameStates.reset;
+    () async {
+      int line = GAME_PAD_MATRIX_H;
+      await Future.doWhile(() async {
+        line--;
+        for (int i = 0; i < GAME_PAD_MATRIX_W; i++) {
+          _data[line][i] = 1;
+        }
+        setState(() {});
+        await Future.delayed(_REST_LINE_DURATION);
+        return line != 0;
+      });
+      _current = null;
+      _getNext();
+      _points = 0;
+      _cleared = 0;
+      await Future.doWhile(() async {
+        for (int i = 0; i < GAME_PAD_MATRIX_W; i++) {
+          _data[line][i] = 0;
+        }
+        setState(() {});
+        line++;
+        await Future.delayed(_REST_LINE_DURATION);
+        return line != GAME_PAD_MATRIX_H;
+      });
+      setState(() {
+        _states = GameStates.none;
+      });
+    }();
+  }
+
+  void _startGame() {
+    if (_states == GameStates.running && _autoFallTimer?.isActive == false) {
+      return;
+    }
+    _states = GameStates.running;
+    _autoFall(true);
+    setState(() {});
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    List<List<int>> mixed = [];
+    for (var i = 0; i < GAME_PAD_MATRIX_H; i++) {
+      mixed.add(List.filled(GAME_PAD_MATRIX_W, 0));
+      for (var j = 0; j < GAME_PAD_MATRIX_W; j++) {
+        int value = _current?.get(j, i) ?? _data[i][j];
+        if (_mask[i][j] == -1) {
+          value = 0;
+        } else if (_mask[i][j] == 1) {
+          value = 2;
+        }
+        mixed[i][j] = value;
+      }
+    }
+    debugPrint("game states : $_states");
+    return GameState(
+        mixed, _states, _level, _sound.mute, _points, _cleared, _next,
+        child: widget.child);
+  }
+
+  void soundSwitch() {
+    setState(() {
+      _sound.mute = !_sound.mute;
+    });
+  }
+}
+
+class GameState extends InheritedWidget {
+  GameState(this.data, this.states, this.level, this.muted, this.points,
+      this.cleared, this.next,
+      {Key key, this.child})
+      : super(key: key, child: child);
+
+  final Widget child;
+
+  ///屏幕展示数据
+  ///0: 空砖块
+  ///1: 普通砖块
+  ///2: 高亮砖块
+  final List<List<int>> data;
+
+  final GameStates states;
+
+  final int level;
+
+  final bool muted;
+
+  final int points;
+
+  final int cleared;
+
+  final Block next;
+
+  static GameState of(BuildContext context) {
+    return (context.inheritFromWidgetOfExactType(GameState) as GameState);
+  }
+
+  @override
+  bool updateShouldNotify(GameState oldWidget) {
+    return true;
+  }
+}

+ 60 - 0
lib/gamer/keyboard.dart

@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'gamer.dart';
+
+///keyboard controller to play game
+class KeyboardController extends StatefulWidget {
+  final Widget child;
+
+  KeyboardController({this.child});
+
+  @override
+  _KeyboardControllerState createState() => _KeyboardControllerState();
+}
+
+class _KeyboardControllerState extends State<KeyboardController> {
+  @override
+  void initState() {
+    super.initState();
+    RawKeyboard.instance.addListener(_onKey);
+  }
+
+  void _onKey(RawKeyEvent event) {
+    if (event is RawKeyUpEvent) {
+      return;
+    }
+
+    final key = event.data.physicalKey;
+    final game = Game.of(context);
+
+    if (key == PhysicalKeyboardKey.arrowUp) {
+      game.rotate();
+    } else if (key == PhysicalKeyboardKey.arrowDown) {
+      game.down();
+    } else if (key == PhysicalKeyboardKey.arrowLeft) {
+      game.left();
+    } else if (key == PhysicalKeyboardKey.arrowRight) {
+      game.right();
+    } else if (key == PhysicalKeyboardKey.space) {
+      game.drop();
+    } else if (key == PhysicalKeyboardKey.keyP) {
+      game.pauseOrResume();
+    } else if (key == PhysicalKeyboardKey.keyS) {
+      game.soundSwitch();
+    } else if (key == PhysicalKeyboardKey.keyR) {
+      game.reset();
+    }
+  }
+
+  @override
+  void dispose() {
+    RawKeyboard.instance.removeListener(_onKey);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.child;
+  }
+}

+ 130 - 0
lib/generated/i18n.dart

@@ -0,0 +1,130 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+// ignore_for_file: non_constant_identifier_names
+// ignore_for_file: camel_case_types
+// ignore_for_file: prefer_single_quotes
+
+//This file is automatically generated. DO NOT EDIT, all your changes would be lost.
+class S implements WidgetsLocalizations {
+  const S();
+
+  static const GeneratedLocalizationsDelegate delegate =
+    GeneratedLocalizationsDelegate();
+
+  static S of(BuildContext context) => Localizations.of<S>(context, S);
+
+  @override
+  TextDirection get textDirection => TextDirection.ltr;
+
+  String get cleans => "Cleans";
+  String get level => "Level";
+  String get next => "Next";
+  String get pause_resume => "PAUSE/RESUME";
+  String get points => "Points";
+  String get reset => "RESET";
+  String get reward => "Reward";
+  String get sounds => "SOUNDS";
+}
+
+class $en extends S {
+  const $en();
+}
+
+class $zh_CN extends S {
+  const $zh_CN();
+
+  @override
+  TextDirection get textDirection => TextDirection.ltr;
+
+  @override
+  String get next => "下一个";
+  @override
+  String get reward => "赞赏";
+  @override
+  String get sounds => "声音";
+  @override
+  String get pause_resume => "暂停/恢复";
+  @override
+  String get level => "级别";
+  @override
+  String get reset => "重置";
+  @override
+  String get cleans => "消除";
+  @override
+  String get points => "分数";
+}
+
+class GeneratedLocalizationsDelegate extends LocalizationsDelegate<S> {
+  const GeneratedLocalizationsDelegate();
+
+  List<Locale> get supportedLocales {
+    return const <Locale>[
+      Locale("en", ""),
+      Locale("zh", "CN"),
+    ];
+  }
+
+  LocaleListResolutionCallback listResolution({Locale fallback}) {
+    return (List<Locale> locales, Iterable<Locale> supported) {
+      if (locales == null || locales.isEmpty) {
+        return fallback ?? supported.first;
+      } else {
+        return _resolve(locales.first, fallback, supported);
+      }
+    };
+  }
+
+  LocaleResolutionCallback resolution({Locale fallback}) {
+    return (Locale locale, Iterable<Locale> supported) {
+      return _resolve(locale, fallback, supported);
+    };
+  }
+
+  Locale _resolve(Locale locale, Locale fallback, Iterable<Locale> supported) {
+    if (locale == null || !isSupported(locale)) {
+      return fallback ?? supported.first;
+    }
+
+    final Locale languageLocale = Locale(locale.languageCode, "");
+    if (supported.contains(locale)) {
+      return locale;
+    } else if (supported.contains(languageLocale)) {
+      return languageLocale;
+    } else {
+      final Locale fallbackLocale = fallback ?? supported.first;
+      return fallbackLocale;
+    }
+  }
+
+  @override
+  Future<S> load(Locale locale) {
+    final String lang = getLang(locale);
+    if (lang != null) {
+      switch (lang) {
+        case "en":
+          return SynchronousFuture<S>(const $en());
+        case "zh_CN":
+          return SynchronousFuture<S>(const $zh_CN());
+        default:
+          // NO-OP.
+      }
+    }
+    return SynchronousFuture<S>(const S());
+  }
+
+  @override
+  bool isSupported(Locale locale) =>
+    locale != null && supportedLocales.contains(locale);
+
+  @override
+  bool shouldReload(GeneratedLocalizationsDelegate old) => false;
+}
+
+String getLang(Locale l) => l == null
+  ? null
+  : l.countryCode != null && l.countryCode.isEmpty
+    ? l.languageCode
+    : l.toString();

+ 118 - 0
lib/income/donation_dialog.dart

@@ -0,0 +1,118 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:overlay_support/overlay_support.dart';
+
+const HONG_BAO = "打开支付宝首页搜“621412820”领红包,领到大红包的小伙伴赶紧使用哦!";
+
+class DonationDialog extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return SimpleDialog(
+      contentPadding:
+          const EdgeInsets.only(top: 8, left: 8, right: 8, bottom: 4),
+      children: <Widget>[
+        SizedBox(width: MediaQuery.of(context).size.width),
+        Container(
+            padding: const EdgeInsets.all(16), child: Text("开发不易,赞助一下开发者。")),
+        _ActionTile(
+          text: "微信捐赠",
+          onTap: () async {
+            await showDialog(
+                context: context,
+                builder: (context) => _ReceiptDialog.weChat());
+            Navigator.pop(context);
+          },
+        ),
+        _ActionTile(
+          text: "支付宝捐赠",
+          onTap: () async {
+            await showDialog(
+                context: context,
+                builder: (context) => _ReceiptDialog.aliPay());
+            Navigator.pop(context);
+          },
+        ),
+        _ActionTile(
+          text: "支付宝红包码",
+          onTap: () async {
+            await Clipboard.setData(ClipboardData(text: HONG_BAO));
+            final data = await Clipboard.getData(Clipboard.kTextPlain);
+            if (data.text == HONG_BAO) {
+              showSimpleNotification(context, Text("已复制到粘贴板 (≧y≦*)"));
+            } else {
+              await showDialog(
+                  context: context,
+                  builder: (context) => _SingleFieldDialog(text: HONG_BAO));
+            }
+            Navigator.of(context).pop();
+          },
+        ),
+      ],
+    );
+  }
+}
+
+class _SingleFieldDialog extends StatelessWidget {
+  final String text;
+
+  const _SingleFieldDialog({Key key, @required this.text}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Dialog(
+      child: Container(
+        padding: EdgeInsets.all(16),
+        child: TextField(
+          maxLines: 5,
+          autofocus: true,
+          controller: TextEditingController(text: text),
+        ),
+      ),
+    );
+  }
+}
+
+class _ReceiptDialog extends StatelessWidget {
+  final String image;
+
+  const _ReceiptDialog({Key key, this.image}) : super(key: key);
+
+  const _ReceiptDialog.weChat() : this(image: "assets/wechat.png");
+
+  const _ReceiptDialog.aliPay() : this(image: "assets/alipay.jpg");
+
+  static final borderRadius = BorderRadius.circular(5);
+
+  @override
+  Widget build(BuildContext context) {
+    return Dialog(
+      shape: RoundedRectangleBorder(borderRadius: borderRadius),
+      child: ClipRRect(borderRadius: borderRadius, child: Image.asset(image)),
+    );
+  }
+}
+
+class _ActionTile extends StatelessWidget {
+  final VoidCallback onTap;
+
+  final String text;
+
+  const _ActionTile({Key key, @required this.onTap, @required this.text})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      onTap: onTap,
+      child: Container(
+        height: 40,
+        child: Row(
+          children: <Widget>[
+            SizedBox(width: 16),
+            Text(text, style: TextStyle(fontWeight: FontWeight.bold)),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 45 - 0
lib/main.dart

@@ -0,0 +1,45 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:tetris/gamer/gamer.dart';
+import 'package:tetris/generated/i18n.dart';
+import 'package:tetris/material/audios.dart';
+import 'package:tetris/pages/home_page.dart';
+import 'package:tetris/utils/app_util.dart';
+
+import 'gamer/keyboard.dart';
+
+void main() {
+  debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
+  AppUtil.disableDebugPrint();
+  runApp(MyApp());
+}
+
+class MyApp extends StatelessWidget {
+  // This widget is the root of your application.
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: '俄罗斯方块',
+      localizationsDelegates: [
+        S.delegate,
+        GlobalMaterialLocalizations.delegate,
+        GlobalWidgetsLocalizations.delegate
+      ],
+      navigatorObservers: [routeObserver],
+      supportedLocales: S.delegate.supportedLocales,
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+      ),
+      home: Scaffold(
+        body: Sound(child: Game(child: KeyboardController(child: HomePage()))),
+      ),
+    );
+  }
+}
+
+final RouteObserver<ModalRoute> routeObserver = RouteObserver<ModalRoute>();
+//边框
+const SCREEN_BORDER_WIDTH = 3.0;
+
+const BACKGROUND_COLOR = const Color(0xffefcc19);

+ 88 - 0
lib/material/audios.dart

@@ -0,0 +1,88 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:soundpool/soundpool.dart';
+
+class Sound extends StatefulWidget {
+  final Widget child;
+
+  const Sound({Key key, this.child}) : super(key: key);
+
+  @override
+  SoundState createState() => SoundState();
+
+  static SoundState of(BuildContext context) {
+    final state = context.ancestorStateOfType(const TypeMatcher<SoundState>());
+    assert(state != null, 'can not find Sound widget');
+    return state;
+  }
+}
+
+const _SOUNDS = [
+  'clean.mp3',
+  'drop.mp3',
+  'explosion.mp3',
+  'move.mp3',
+  'rotate.mp3',
+  'start.mp3'
+];
+
+class SoundState extends State<Sound> {
+  Soundpool _pool;
+
+  Map<String, int> _soundIds;
+
+  bool mute = false;
+
+  void _play(String name) {
+    final soundId = _soundIds[name];
+    if (soundId != null && !mute) {
+      _pool.play(soundId);
+    }
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _pool = Soundpool(streamType: StreamType.music, maxStreams: 4);
+    _soundIds = Map();
+    for (var value in _SOUNDS) {
+      scheduleMicrotask(() async {
+        final data = await rootBundle.load('assets/audios/$value');
+        _soundIds[value] = await _pool.load(data);
+      });
+    }
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _pool.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.child;
+  }
+
+  void start() {
+    _play('start.mp3');
+  }
+
+  void clear() {
+    _play('clean.mp3');
+  }
+
+  void fall() {
+    _play('drop.mp3');
+  }
+
+  void rotate() {
+    _play('rotate.mp3');
+  }
+
+  void move() {
+    _play('move.mp3');
+  }
+}

+ 59 - 0
lib/material/briks.dart

@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+
+const _COLOR_NORMAL = Colors.black87;
+
+const _COLOR_NULL = Colors.black12;
+
+const _COLOR_HIGHLIGHT = Color(0xFF560000);
+
+class BrikSize extends InheritedWidget {
+  const BrikSize({
+    Key key,
+    @required this.size,
+    @required Widget child,
+  })  : assert(child != null),
+        super(key: key, child: child);
+
+  final Size size;
+
+  static BrikSize of(BuildContext context) {
+    final brikSize = context.inheritFromWidgetOfExactType(BrikSize) as BrikSize;
+    assert(brikSize != null, "....");
+    return brikSize;
+  }
+
+  @override
+  bool updateShouldNotify(BrikSize old) {
+    return old.size != size;
+  }
+}
+
+///the basic brik for game panel
+class Brik extends StatelessWidget {
+  final Color color;
+
+  const Brik._({Key key, this.color}) : super(key: key);
+
+  const Brik.normal() : this._(color: _COLOR_NORMAL);
+
+  const Brik.empty() : this._(color: _COLOR_NULL);
+
+  const Brik.highlight() : this._(color: _COLOR_HIGHLIGHT);
+
+  @override
+  Widget build(BuildContext context) {
+    final width = BrikSize.of(context).size.width;
+    return SizedBox.fromSize(
+      size: BrikSize.of(context).size,
+      child: Container(
+        margin: EdgeInsets.all(0.05 * width),
+        padding: EdgeInsets.all(0.1 * width),
+        decoration:
+            BoxDecoration(border: Border.all(width: 0.10 * width, color: color)),
+        child: Container(
+          color: color,
+        ),
+      ),
+    );
+  }
+}

+ 250 - 0
lib/material/images.dart

@@ -0,0 +1,250 @@
+import 'dart:async';
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+
+import 'material.dart';
+
+const _DIGITAL_ROW_SIZE = Size(14, 24);
+
+class Number extends StatelessWidget {
+  final int length;
+
+  ///the number to show
+  ///could be null
+  final int number;
+
+  final bool padWithZero;
+
+  Number(
+      {Key key,
+      this.length = 5,
+      @required this.number,
+      this.padWithZero = false})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    String digitalStr = number?.toString() ?? "";
+    if (digitalStr.length > length) {
+      digitalStr = digitalStr.substring(digitalStr.length - length);
+    }
+    digitalStr = digitalStr.padLeft(length, padWithZero ? "0" : " ");
+    List<Widget> children = [];
+    for (int i = 0; i < length; i++) {
+      children.add(Digital(int.tryParse(digitalStr[i])));
+    }
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: children,
+    );
+  }
+}
+
+class IconDragon extends StatefulWidget {
+  final bool animate;
+
+  const IconDragon({Key key, this.animate = false}) : super(key: key);
+
+  @override
+  _IconDragonState createState() {
+    return new _IconDragonState();
+  }
+}
+
+class _IconDragonState extends State<IconDragon> {
+  Timer _timer;
+
+  @override
+  void didUpdateWidget(IconDragon oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    _initAnimation();
+  }
+
+  ///current frame of animation
+  int _frame = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    _initAnimation();
+  }
+
+  void _initAnimation() {
+    _timer?.cancel();
+    _timer = null;
+    if (!widget.animate) {
+      return;
+    }
+    _timer = Timer.periodic(const Duration(milliseconds: 200), (t) {
+      if (_frame > 30) {
+        _frame = 0;
+      }
+      setState(() {
+        _frame++;
+      });
+    });
+  }
+
+  @override
+  void dispose() {
+    _timer?.cancel();
+    _timer = null;
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return _Material(
+      size: const Size(80, 86),
+      srcSize: const Size(80, 86),
+      srcOffset: _getOffset(_frame),
+    );
+  }
+
+  Offset _getOffset(int frame) {
+    int index = 0;
+    if (frame < 10) {
+      index = frame % 2 == 0 ? 0 : 1;
+    } else {
+      index = frame % 2 == 0 ? 2 : 3;
+    }
+    double dx = index * 100.0;
+    return Offset(dx, 100);
+  }
+}
+
+class IconPause extends StatelessWidget {
+  final bool enable;
+  final Size size;
+
+  const IconPause({Key key, this.enable = true, this.size = const Size(18, 16)})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return _Material(
+      size: size,
+      srcSize: const Size(20, 18),
+      srcOffset: enable ? const Offset(75, 75) : const Offset(100, 75),
+    );
+  }
+}
+
+class IconSound extends StatelessWidget {
+  final bool enable;
+  final Size size;
+
+  const IconSound({Key key, this.enable = true, this.size = const Size(18, 16)})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return _Material(
+      size: size,
+      srcSize: const Size(25, 21),
+      srcOffset: enable ? const Offset(150, 75) : const Offset(175, 75),
+    );
+  }
+}
+
+class IconColon extends StatelessWidget {
+  final bool enable;
+
+  final Size size;
+
+  const IconColon({Key key, this.enable = true, this.size = const Size(10, 17)})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return _Material(
+      size: size,
+      srcOffset: enable ? const Offset(229, 25) : const Offset(243, 25),
+      srcSize: _DIGITAL_ROW_SIZE,
+    );
+  }
+}
+
+/// a single digital
+class Digital extends StatelessWidget {
+  ///number 0 - 9
+  ///or null indicate it is invalid
+  final int digital;
+
+  final Size size;
+
+  Digital(this.digital, {Key key, this.size = const Size(10, 17)})
+      : assert(digital == null || (digital <= 9 && digital >= 0)),
+        super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return _Material(
+      size: size,
+      srcOffset: _getDigitalOffset(),
+      srcSize: _DIGITAL_ROW_SIZE,
+    );
+  }
+
+  Offset _getDigitalOffset() {
+    int offset = digital ?? 10;
+    final dx = 75.0 + 14 * offset;
+    return Offset(dx, 25);
+  }
+}
+
+class _Material extends StatelessWidget {
+  //the size off widget
+  final Size size;
+
+  final Size srcSize;
+
+  final Offset srcOffset;
+
+  const _Material(
+      {Key key,
+      @required this.size,
+      @required this.srcSize,
+      @required this.srcOffset})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return CustomPaint(
+      foregroundPainter: _MaterialPainter(
+          srcOffset, srcSize, GameMaterial.getMaterial(context)),
+      child: SizedBox.fromSize(
+        size: size,
+      ),
+    );
+  }
+}
+
+class _MaterialPainter extends CustomPainter {
+  ///offset to adjust the drawing
+  final Offset offset;
+
+  ///the size we pick from [_material]
+  final Size size;
+
+  final ui.Image material;
+
+  _MaterialPainter(this.offset, this.size, this.material);
+
+  Paint _paint = Paint();
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final src =
+        Rect.fromLTWH(offset.dx, offset.dy, this.size.width, this.size.height);
+    canvas.scale(size.width / this.size.width, size.height / this.size.height);
+    canvas.drawImageRect(material, src,
+        Rect.fromLTWH(0, 0, this.size.width, this.size.height), _paint);
+  }
+
+  @override
+  bool shouldRepaint(_MaterialPainter oldDelegate) {
+    return oldDelegate.offset != offset || oldDelegate.size != size;
+  }
+}

+ 48 - 0
lib/material/material.dart

@@ -0,0 +1,48 @@
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+class GameMaterial extends StatefulWidget {
+  final Widget child;
+
+  const GameMaterial({Key key, this.child}) : super(key: key);
+
+  @override
+  _GameMaterialState createState() => _GameMaterialState();
+
+  static ui.Image getMaterial(BuildContext context) {
+    final _GameMaterialState state =
+        context.ancestorStateOfType(const TypeMatcher<_GameMaterialState>());
+    assert(state != null, "can not find GameMaterial widget");
+    return state.material;
+  }
+}
+
+class _GameMaterialState extends State<GameMaterial> {
+  ///the image data of /assets/material.png
+  ui.Image material;
+
+  @override
+  void initState() {
+    super.initState();
+    _doLoadMaterial();
+  }
+
+  void _doLoadMaterial() async {
+    if (material != null) {
+      return;
+    }
+    final bytes = await rootBundle.load("assets/material.png");
+    final codec = await ui.instantiateImageCodec(bytes.buffer.asUint8List());
+    final frame = await codec.getNextFrame();
+    setState(() {
+      material = frame.image;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return material == null ? Container() : widget.child;
+  }
+}

+ 3 - 0
lib/model/config.dart

@@ -0,0 +1,3 @@
+class Config {
+
+}

+ 13 - 0
lib/pages/home_page.dart

@@ -0,0 +1,13 @@
+import 'package:flutter/material.dart';
+import 'package:tetris/panel/page_portrait.dart';
+
+class HomePage extends StatelessWidget {
+  const HomePage({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    //only Android/iOS support land mode
+    bool land = MediaQuery.of(context).orientation == Orientation.landscape;
+    return land ? PageLand() : PagePortrait();
+  }
+}

+ 368 - 0
lib/panel/controller.dart

@@ -0,0 +1,368 @@
+import 'dart:async';
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:tetris/gamer/gamer.dart';
+import 'package:tetris/generated/i18n.dart';
+
+class GameController extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 200,
+      child: Row(
+        children: <Widget>[
+          Expanded(child: LeftController()),
+          Expanded(child: DirectionController()),
+        ],
+      ),
+    );
+  }
+}
+
+const Size _DIRECTION_BUTTON_SIZE = const Size(48, 48);
+
+const Size _SYSTEM_BUTTON_SIZE = const Size(28, 28);
+
+const double _DIRECTION_SPACE = 16;
+
+const double _iconSize = 16;
+
+class DirectionController extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      alignment: Alignment.center,
+      children: <Widget>[
+        SizedBox.fromSize(size: _DIRECTION_BUTTON_SIZE * 2.8),
+        Transform.rotate(
+          angle: math.pi / 4,
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: <Widget>[
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  Transform.scale(
+                    scale: 1.5,
+                    child: Transform.rotate(
+                        angle: -math.pi / 4,
+                        child: Icon(
+                          Icons.arrow_drop_up,
+                          size: _iconSize,
+                        )),
+                  ),
+                  Transform.scale(
+                    scale: 1.5,
+                    child: Transform.rotate(
+                        angle: -math.pi / 4,
+                        child: Icon(
+                          Icons.arrow_right,
+                          size: _iconSize,
+                        )),
+                  ),
+                ],
+              ),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  Transform.scale(
+                    scale: 1.5,
+                    child: Transform.rotate(
+                        angle: -math.pi / 4,
+                        child: Icon(
+                          Icons.arrow_left,
+                          size: _iconSize,
+                        )),
+                  ),
+                  Transform.scale(
+                    scale: 1.5,
+                    child: Transform.rotate(
+                        angle: -math.pi / 4,
+                        child: Icon(
+                          Icons.arrow_drop_down,
+                          size: _iconSize,
+                        )),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ),
+        Transform.rotate(
+          angle: math.pi / 4,
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: <Widget>[
+              SizedBox(height: _DIRECTION_SPACE),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  _Button(
+                      enableLongPress: false,
+                      size: _DIRECTION_BUTTON_SIZE,
+                      onTap: () {
+                        Game.of(context).rotate();
+                      }),
+                  SizedBox(width: _DIRECTION_SPACE),
+                  _Button(
+                      size: _DIRECTION_BUTTON_SIZE,
+                      onTap: () {
+                        Game.of(context).right();
+                      }),
+                ],
+              ),
+              SizedBox(height: _DIRECTION_SPACE),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  _Button(
+                      size: _DIRECTION_BUTTON_SIZE,
+                      onTap: () {
+                        Game.of(context).left();
+                      }),
+                  SizedBox(width: _DIRECTION_SPACE),
+                  _Button(
+                    size: _DIRECTION_BUTTON_SIZE,
+                    onTap: () {
+                      Game.of(context).down();
+                    },
+                  ),
+                ],
+              ),
+              SizedBox(height: _DIRECTION_SPACE),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class SystemButtonGroup extends StatelessWidget {
+  static const _systemButtonColor = const Color(0xFF2dc421);
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+      children: <Widget>[
+        _Description(
+          text: S.of(context).sounds,
+          child: _Button(
+              size: _SYSTEM_BUTTON_SIZE,
+              color: _systemButtonColor,
+              enableLongPress: false,
+              onTap: () {
+                Game.of(context).soundSwitch();
+              }),
+        ),
+        _Description(
+          text: S.of(context).pause_resume,
+          child: _Button(
+              size: _SYSTEM_BUTTON_SIZE,
+              color: _systemButtonColor,
+              enableLongPress: false,
+              onTap: () {
+                Game.of(context).pauseOrResume();
+              }),
+        ),
+        _Description(
+          text: S.of(context).reset,
+          child: _Button(
+              size: _SYSTEM_BUTTON_SIZE,
+              enableLongPress: false,
+              color: Colors.red,
+              onTap: () {
+                Game.of(context).reset();
+              }),
+        )
+      ],
+    );
+  }
+}
+
+class DropButton extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return _Description(
+      text: 'drop',
+      child: _Button(
+          enableLongPress: false,
+          size: Size(90, 90),
+          onTap: () {
+            Game.of(context).drop();
+          }),
+    );
+  }
+}
+
+class LeftController extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: <Widget>[
+        SystemButtonGroup(),
+        Expanded(
+          child: Center(
+            child: DropButton(),
+          ),
+        )
+      ],
+    );
+  }
+}
+
+class _Button extends StatefulWidget {
+  final Size size;
+  final Widget icon;
+
+  final VoidCallback onTap;
+
+  ///the color of button
+  final Color color;
+
+  final bool enableLongPress;
+
+  const _Button(
+      {Key key,
+      @required this.size,
+      @required this.onTap,
+      this.icon,
+      this.color = Colors.blue,
+      this.enableLongPress = true})
+      : super(key: key);
+
+  @override
+  _ButtonState createState() {
+    return new _ButtonState();
+  }
+}
+
+///show a hint text for child widget
+class _Description extends StatelessWidget {
+  final String text;
+
+  final Widget child;
+
+  final AxisDirection direction;
+
+  const _Description({
+    Key key,
+    this.text,
+    this.direction = AxisDirection.down,
+    this.child,
+  })  : assert(direction != null),
+        super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    Widget widget;
+    switch (direction) {
+      case AxisDirection.right:
+        widget = Row(
+            mainAxisSize: MainAxisSize.min,
+            children: <Widget>[child, SizedBox(width: 8), Text(text)]);
+        break;
+      case AxisDirection.left:
+        widget = Row(
+          children: <Widget>[Text(text), SizedBox(width: 8), child],
+          mainAxisSize: MainAxisSize.min,
+        );
+        break;
+      case AxisDirection.up:
+        widget = Column(
+          children: <Widget>[Text(text), SizedBox(height: 8), child],
+          mainAxisSize: MainAxisSize.min,
+        );
+        break;
+      case AxisDirection.down:
+        widget = Column(
+          children: <Widget>[child, SizedBox(height: 8), Text(text)],
+          mainAxisSize: MainAxisSize.min,
+        );
+        break;
+    }
+    return DefaultTextStyle(
+      child: widget,
+      style: TextStyle(fontSize: 12, color: Colors.black),
+    );
+  }
+}
+
+class _ButtonState extends State<_Button> {
+  Timer _timer;
+
+  bool _tapEnded = false;
+
+  Color _color;
+
+  @override
+  void didUpdateWidget(_Button oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    _color = widget.color;
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _color = widget.color;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Material(
+      color: _color,
+      elevation: 2,
+      shape: CircleBorder(),
+      child: GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onTapDown: (_) async {
+          setState(() {
+            _color = widget.color.withOpacity(0.5);
+          });
+          if (_timer != null) {
+            return;
+          }
+          _tapEnded = false;
+          widget.onTap();
+          if (!widget.enableLongPress) {
+            return;
+          }
+          await Future.delayed(const Duration(milliseconds: 300));
+          if (_tapEnded) {
+            return;
+          }
+          _timer = Timer.periodic(const Duration(milliseconds: 60), (t) {
+            if (!_tapEnded) {
+              widget.onTap();
+            } else {
+              t.cancel();
+              _timer = null;
+            }
+          });
+        },
+        onTapCancel: () {
+          _tapEnded = true;
+          _timer?.cancel();
+          _timer = null;
+          setState(() {
+            _color = widget.color;
+          });
+        },
+        onTapUp: (_) {
+          _tapEnded = true;
+          _timer?.cancel();
+          _timer = null;
+          setState(() {
+            _color = widget.color;
+          });
+        },
+        child: SizedBox.fromSize(
+          size: widget.size,
+        ),
+      ),
+    );
+  }
+}

+ 58 - 0
lib/panel/page_land.dart

@@ -0,0 +1,58 @@
+part of 'page_portrait.dart';
+
+class PageLand extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    var height = MediaQuery.of(context).size.height;
+    height -= MediaQuery.of(context).viewInsets.vertical;
+    return SizedBox.expand(
+      child: Container(
+        color: BACKGROUND_COLOR,
+        child: Padding(
+          padding: MediaQuery.of(context).padding,
+          child: Row(
+            mainAxisSize: MainAxisSize.min,
+            children: <Widget>[
+              Expanded(
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: <Widget>[
+                    Spacer(),
+                    SystemButtonGroup(),
+                    Spacer(),
+                    Padding(
+                      padding: const EdgeInsets.only(left: 40, bottom: 40),
+                      child: DropButton(),
+                    )
+                  ],
+                ),
+              ),
+              _ScreenDecoration(child: Screen.fromHeight(height * 0.8)),
+              Expanded(
+                child: Column(
+                  children: <Widget>[
+                    Row(
+                      children: <Widget>[
+                        Spacer(),
+                        FlatButton(
+                            onPressed: () {
+                              showDialog(
+                                  context: context,
+                                  builder: (context) => DonationDialog());
+                            },
+                            child: Text(S.of(context).reward))
+                      ],
+                    ),
+                    Spacer(),
+                    DirectionController(),
+                    SizedBox(height: 30),
+                  ],
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 77 - 0
lib/panel/page_portrait.dart

@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'package:tetris/generated/i18n.dart';
+import 'package:tetris/income/donation_dialog.dart';
+import 'package:tetris/main.dart';
+import 'package:tetris/panel/controller.dart';
+import 'package:tetris/panel/screen.dart';
+
+part 'page_land.dart';
+
+class PagePortrait extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    final size = MediaQuery.of(context).size;
+    final screenW = size.width * 0.8;
+
+    return SizedBox.expand(
+      child: Container(
+        color: BACKGROUND_COLOR,
+        child: Padding(
+          padding: MediaQuery.of(context).padding,
+          child: Column(
+            children: <Widget>[
+              Row(
+                children: <Widget>[
+                  Spacer(),
+                  FlatButton(
+                      onPressed: () {
+                        showDialog(
+                            context: context,
+                            builder: (context) => DonationDialog());
+                      },
+                      child: Text(S.of(context).reward))
+                ],
+              ),
+              Spacer(),
+              _ScreenDecoration(child: Screen(width: screenW)),
+              Spacer(flex: 2),
+              GameController(),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class _ScreenDecoration extends StatelessWidget {
+  final Widget child;
+
+  const _ScreenDecoration({Key key, @required this.child}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        border: Border(
+          top: BorderSide(
+              color: const Color(0xFF987f0f), width: SCREEN_BORDER_WIDTH),
+          left: BorderSide(
+              color: const Color(0xFF987f0f), width: SCREEN_BORDER_WIDTH),
+          right: BorderSide(
+              color: const Color(0xFFfae36c), width: SCREEN_BORDER_WIDTH),
+          bottom: BorderSide(
+              color: const Color(0xFFfae36c), width: SCREEN_BORDER_WIDTH),
+        ),
+      ),
+      child: Container(
+        decoration: BoxDecoration(border: Border.all(color: Colors.black54)),
+        child: Container(
+          padding: const EdgeInsets.all(3),
+          color: SCREEN_BACKGROUND,
+          child: child,
+        ),
+      ),
+    );
+  }
+}

+ 81 - 0
lib/panel/player_panel.dart

@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'package:tetris/material/briks.dart';
+import 'package:tetris/material/images.dart';
+import 'package:tetris/gamer/gamer.dart';
+
+const _PLAYER_PANEL_PADDING = 6;
+
+Size getBrikSizeForScreenWidth(double width) {
+  return Size.square((width - _PLAYER_PANEL_PADDING) / GAME_PAD_MATRIX_W);
+}
+
+///the matrix of player content
+class PlayerPanel extends StatelessWidget {
+  //the size of player panel
+  final Size size;
+
+  PlayerPanel({Key key, @required double width})
+      : assert(width != null && width != 0),
+        size = Size(width, width * 2),
+        super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    debugPrint("size : $size");
+    return SizedBox.fromSize(
+      size: size,
+      child: Container(
+        padding: EdgeInsets.all(2),
+        decoration: BoxDecoration(
+          border: Border.all(color: Colors.black),
+        ),
+        child: Stack(
+          children: <Widget>[
+            _PlayerPad(),
+            _GameUninitialized(),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class _PlayerPad extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: GameState.of(context).data.map((list) {
+        return Row(
+          children: list.map((b) {
+            return b == 1
+                ? const Brik.normal()
+                : b == 2 ? const Brik.highlight() : const Brik.empty();
+          }).toList(),
+        );
+      }).toList(),
+    );
+  }
+}
+
+class _GameUninitialized extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    if (GameState.of(context).states == GameStates.none) {
+      return Center(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: <Widget>[
+            IconDragon(animate: true),
+            SizedBox(height: 16),
+            Text(
+              "tetrix",
+              style: TextStyle(fontSize: 20),
+            ),
+          ],
+        ),
+      );
+    } else {
+      return Container();
+    }
+  }
+}

+ 108 - 0
lib/panel/screen.dart

@@ -0,0 +1,108 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:tetris/gamer/gamer.dart';
+import 'package:tetris/material/briks.dart';
+import 'package:tetris/material/material.dart';
+import 'package:vector_math/vector_math_64.dart' as v;
+
+import 'player_panel.dart';
+import 'status_panel.dart';
+
+const Color SCREEN_BACKGROUND = Color(0xff9ead86);
+
+/// screen H : W;
+class Screen extends StatelessWidget {
+  ///the with of screen
+  final double width;
+
+  const Screen({Key key, @required this.width}) : super(key: key);
+
+  Screen.fromHeight(double height) : this(width: ((height - 6) / 2 + 6) / 0.6);
+
+  @override
+  Widget build(BuildContext context) {
+    //play panel need 60%
+    final playerPanelWidth = width * 0.6;
+    return Shake(
+      shake: GameState.of(context).states == GameStates.drop,
+      child: SizedBox(
+        height: (playerPanelWidth - 6) * 2 + 6,
+        width: width,
+        child: Container(
+          color: SCREEN_BACKGROUND,
+          child: GameMaterial(
+            child: BrikSize(
+              size: getBrikSizeForScreenWidth(playerPanelWidth),
+              child: Row(
+                children: <Widget>[
+                  PlayerPanel(width: playerPanelWidth),
+                  SizedBox(
+                    width: width - playerPanelWidth,
+                    child: StatusPanel(),
+                  )
+                ],
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class Shake extends StatefulWidget {
+  final Widget child;
+
+  ///true to shake screen vertically
+  final bool shake;
+
+  const Shake({Key key, @required this.child, @required this.shake})
+      : super(key: key);
+
+  @override
+  _ShakeState createState() => _ShakeState();
+}
+
+///摇晃屏幕
+class _ShakeState extends State<Shake> with TickerProviderStateMixin {
+  AnimationController _controller;
+
+  @override
+  void initState() {
+    _controller =
+        AnimationController(vsync: this, duration: Duration(milliseconds: 150))
+          ..addListener(() {
+            setState(() {});
+          });
+    super.initState();
+  }
+
+  @override
+  void didUpdateWidget(Shake oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.shake) {
+      _controller.forward(from: 0);
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  v.Vector3 _getTranslation() {
+    double progress = _controller.value;
+    double offset = sin(progress * pi) * 1.5;
+    return v.Vector3(0, offset, 0.0);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Transform(
+      transform: Matrix4.translation(_getTranslation()),
+      child: widget.child,
+    );
+  }
+}

+ 116 - 0
lib/panel/status_panel.dart

@@ -0,0 +1,116 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:tetris/gamer/block.dart';
+import 'package:tetris/gamer/gamer.dart';
+import 'package:tetris/generated/i18n.dart';
+import 'package:tetris/material/briks.dart';
+import 'package:tetris/material/images.dart';
+
+class StatusPanel extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.all(8),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: <Widget>[
+          Text(S.of(context).points,
+              style: TextStyle(fontWeight: FontWeight.bold)),
+          SizedBox(height: 4),
+          Number(number: GameState.of(context).points),
+          SizedBox(height: 10),
+          Text(S.of(context).cleans,
+              style: TextStyle(fontWeight: FontWeight.bold)),
+          SizedBox(height: 4),
+          Number(number: GameState.of(context).cleared),
+          SizedBox(height: 10),
+          Text(S.of(context).level,
+              style: TextStyle(fontWeight: FontWeight.bold)),
+          SizedBox(height: 4),
+          Number(number: GameState.of(context).level),
+          SizedBox(height: 10),
+          Text(S.of(context).next,
+              style: TextStyle(fontWeight: FontWeight.bold)),
+          SizedBox(height: 4),
+          _NextBlock(),
+          Spacer(),
+          _GameStatus(),
+        ],
+      ),
+    );
+  }
+}
+
+class _NextBlock extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    List<List<int>> data = [List.filled(4, 0), List.filled(4, 0)];
+    final next = BLOCK_SHAPES[GameState.of(context).next.type];
+    for (int i = 0; i < next.length; i++) {
+      for (int j = 0; j < next[i].length; j++) {
+        data[i][j] = next[i][j];
+      }
+    }
+    return Column(
+      children: data.map((list) {
+        return Row(
+          children: list.map((b) {
+            return b == 1 ? const Brik.normal() : const Brik.empty();
+          }).toList(),
+        );
+      }).toList(),
+    );
+  }
+}
+
+class _GameStatus extends StatefulWidget {
+  @override
+  _GameStatusState createState() {
+    return new _GameStatusState();
+  }
+}
+
+class _GameStatusState extends State<_GameStatus> {
+  Timer _timer;
+
+  bool _colonEnable = true;
+
+  int _minute;
+
+  int _hour;
+
+  @override
+  void initState() {
+    super.initState();
+    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
+      final now = DateTime.now();
+      setState(() {
+        _colonEnable = !_colonEnable;
+        _minute = now.minute;
+        _hour = now.hour;
+      });
+    });
+  }
+
+  @override
+  void dispose() {
+    _timer?.cancel();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: <Widget>[
+        IconSound(enable: GameState.of(context).muted),
+        SizedBox(width: 4),
+        IconPause(enable: GameState.of(context).states == GameStates.paused),
+        Spacer(),
+        Number(number: _hour, length: 2, padWithZero: true),
+        IconColon(enable: _colonEnable),
+        Number(number: _minute, length: 2, padWithZero: true),
+      ],
+    );
+  }
+}

+ 18 - 0
lib/utils/app_util.dart

@@ -0,0 +1,18 @@
+import 'package:flutter/foundation.dart';
+
+class AppUtil {
+
+  // 关闭打印
+  static void disableDebugPrint() {
+    bool debug = false;
+    assert(() {
+      debug = true;
+      return true;
+    }());
+    if (!debug) {
+      debugPrint = (String message, {int wrapWidth}) {
+        //disable log print when not in debug mode
+      };
+    }
+  }
+}

+ 1 - 0
linux/.gitignore

@@ -0,0 +1 @@
+.generated_flutter_root

+ 143 - 0
linux/Makefile

@@ -0,0 +1,143 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Example-specific variables.
+# To modify this Makefile for a different application, these are the values
+# that are mostly likely to need to be changed.
+
+# Executable name.
+BINARY_NAME=flutter_desktop_example
+# The C++ code for the embedder application.
+SOURCES=flutter_embedder_example.cc
+
+
+# Default build type. For a release build, set BUILD=release.
+# Currently this only sets NDEBUG, which is used to control the flags passed
+# to the Flutter engine in the example shell, and not the complation settings
+# (e.g., optimization level) of the C++ code.
+BUILD=debug
+
+# Dependency locations
+FLUTTER_APP_DIR=$(CURDIR)/..
+FLUTTER_APP_BUILD_DIR=$(FLUTTER_APP_DIR)/build
+FLUTTER_ROOT:=$(shell cat $(CURDIR)/.generated_flutter_root)
+FLUTTER_ARTIFACT_CACHE_DIR=$(FLUTTER_ROOT)/bin/cache/artifacts/engine/linux-x64
+
+OUT_DIR=$(FLUTTER_APP_BUILD_DIR)/linux
+CACHE_DIR=$(OUT_DIR)/cache
+FLUTTER_LIBRARY_COPY_DIR=$(CACHE_DIR)/flutter_library
+
+FLUTTER_LIB_NAME=flutter_linux
+FLUTTER_LIB=$(FLUTTER_LIBRARY_COPY_DIR)/lib$(FLUTTER_LIB_NAME).so
+
+# Tools
+FLUTTER_BIN=$(FLUTTER_ROOT)/bin/flutter
+
+# Resources
+ICU_DATA_NAME=icudtl.dat
+ICU_DATA_SOURCE=$(FLUTTER_ARTIFACT_CACHE_DIR)/$(ICU_DATA_NAME)
+FLUTTER_ASSETS_NAME=flutter_assets
+FLUTTER_ASSETS_SOURCE=$(FLUTTER_APP_BUILD_DIR)/$(FLUTTER_ASSETS_NAME)
+
+# Bundle structure
+BUNDLE_OUT_DIR=$(OUT_DIR)/$(BUILD)
+BUNDLE_DATA_DIR=$(BUNDLE_OUT_DIR)/data
+BUNDLE_LIB_DIR=$(BUNDLE_OUT_DIR)/lib
+
+BIN_OUT=$(BUNDLE_OUT_DIR)/$(BINARY_NAME)
+ICU_DATA_OUT=$(BUNDLE_DATA_DIR)/$(ICU_DATA_NAME)
+FLUTTER_LIB_OUT=$(BUNDLE_LIB_DIR)/$(notdir $(FLUTTER_LIB))
+
+# Add relevant code from the wrapper library, which is intended to be statically
+# built into the client.
+WRAPPER_ROOT=$(FLUTTER_LIBRARY_COPY_DIR)/cpp_client_wrapper
+WRAPPER_SOURCES= \
+	$(WRAPPER_ROOT)/flutter_window_controller.cc \
+	$(WRAPPER_ROOT)/plugin_registrar.cc \
+	$(WRAPPER_ROOT)/engine_method_result.cc
+SOURCES+=$(WRAPPER_SOURCES)
+
+# Headers
+WRAPPER_INCLUDE_DIR=$(WRAPPER_ROOT)/include
+INCLUDE_DIRS=$(FLUTTER_LIBRARY_COPY_DIR) $(WRAPPER_INCLUDE_DIR)
+
+# The stamp file created by copy_flutter_files.
+FLUTTER_COPY_STAMP_FILE=$(FLUTTER_LIBRARY_COPY_DIR)/.last_copied_flutter_version
+# The Flutter engine version file.
+FLUTTER_ENGINE_VERSION_FILE=$(FLUTTER_ROOT)/bin/internal/engine.version
+
+# Build settings
+CXX=g++ -std=c++14
+CXXFLAGS.release=-DNDEBUG
+CXXFLAGS=-Wall -Werror $(CXXFLAGS.$(BUILD))
+CPPFLAGS=$(patsubst %,-I%,$(INCLUDE_DIRS))
+LDFLAGS=-L$(BUNDLE_LIB_DIR) \
+	-l$(FLUTTER_LIB_NAME) \
+	-Wl,-rpath=\$$ORIGIN/lib
+
+# Targets
+
+.PHONY: all
+all: $(BIN_OUT) bundle
+
+.PHONY: bundle
+bundle: $(ICU_DATA_OUT) $(FLUTTER_LIB_OUT) bundleflutterassets
+
+$(BIN_OUT): $(SOURCES) $(FLUTTER_LIB_OUT)
+	mkdir -p $(@D)
+	$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(SOURCES) $(LDFLAGS) -o $@
+
+$(WRAPPER_SOURCES) $(FLUTTER_LIB) $(ICU_DATA_SOURCE): $(FLUTTER_COPY_STAMP_FILE)
+
+# When the Flutter engine version changes, the local copy of engine artifacts
+# needs to be updated.
+$(FLUTTER_COPY_STAMP_FILE): $(FLUTTER_ENGINE_VERSION_FILE)
+	$(FLUTTER_BIN) precache --linux --no-android --no-ios
+	rm -rf $(FLUTTER_LIBRARY_COPY_DIR)
+	mkdir -p $(FLUTTER_LIBRARY_COPY_DIR)
+	cp $(FLUTTER_ARTIFACT_CACHE_DIR)/*.h $(FLUTTER_LIBRARY_COPY_DIR)/
+	cp $(FLUTTER_ARTIFACT_CACHE_DIR)/*.so $(FLUTTER_LIBRARY_COPY_DIR)/
+	cp -r $(FLUTTER_ARTIFACT_CACHE_DIR)/cpp_client_wrapper \
+		$(FLUTTER_LIBRARY_COPY_DIR)/
+	touch $(FLUTTER_COPY_STAMP_FILE)
+
+$(FLUTTER_LIB_OUT): $(FLUTTER_LIB)
+	mkdir -p $(BUNDLE_LIB_DIR)
+	cp $(FLUTTER_LIB) $(BUNDLE_LIB_DIR)
+
+$(ICU_DATA_OUT): $(ICU_DATA_SOURCE)
+	mkdir -p $(dir $(ICU_DATA_OUT))
+	cp $(ICU_DATA_SOURCE) $(ICU_DATA_OUT)
+
+# Fully re-copy the assets directory on each build to avoid having to keep a
+# comprehensive list of all asset files here, which would be fragile to changes
+# in the Flutter example (e.g., adding a new font to pubspec.yaml would require
+# changes here).
+.PHONY: bundleflutterassets
+bundleflutterassets: $(FLUTTER_ASSETS_SOURCE)
+	mkdir -p $(BUNDLE_DATA_DIR)
+	rsync -rpu --delete $(FLUTTER_ASSETS_SOURCE) $(BUNDLE_DATA_DIR)
+
+# PHONY since the Makefile doesn't have all the dependency information necessary
+# to know if 'build bundle' needs to be re-run.
+.PHONY: $(FLUTTER_ASSETS_SOURCE)
+$(FLUTTER_ASSETS_SOURCE):
+	cd $(FLUTTER_APP_DIR); \
+	$(FLUTTER_BIN) build bundle $(FLUTTER_BUNDLE_FLAGS)
+
+.PHONY: clean
+clean:
+	rm -rf $(OUT_DIR); \
+	cd $(FLUTTER_APP_DIR); \
+	$(FLUTTER_BIN) clean

+ 11 - 0
linux/flutter/generated_plugin_registrant.cc

@@ -0,0 +1,11 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+}

Some files were not shown because too many files changed in this diff