Selaa lähdekoodia

first commit: 完成核心算法

guoziyun 7 kuukautta sitten
sitoutus
7fdeea183c
100 muutettua tiedostoa jossa 5188 lisäystä ja 0 poistoa
  1. 45 0
      .gitignore
  2. 45 0
      .metadata
  3. 25 0
      .vscode/launch.json
  4. 16 0
      README.md
  5. 28 0
      analysis_options.yaml
  6. 14 0
      android/.gitignore
  7. 44 0
      android/app/build.gradle.kts
  8. 7 0
      android/app/src/debug/AndroidManifest.xml
  9. 45 0
      android/app/src/main/AndroidManifest.xml
  10. 5 0
      android/app/src/main/kotlin/com/example/image_puzzle/MainActivity.kt
  11. 12 0
      android/app/src/main/res/drawable-v21/launch_background.xml
  12. 12 0
      android/app/src/main/res/drawable/launch_background.xml
  13. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  14. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  15. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  16. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  17. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  18. 18 0
      android/app/src/main/res/values-night/styles.xml
  19. 18 0
      android/app/src/main/res/values/styles.xml
  20. 7 0
      android/app/src/profile/AndroidManifest.xml
  21. 21 0
      android/build.gradle.kts
  22. 3 0
      android/gradle.properties
  23. 5 0
      android/gradle/wrapper/gradle-wrapper.properties
  24. 25 0
      android/settings.gradle.kts
  25. BIN
      assets/icons/test.jpg
  26. BIN
      assets/images/opt.jpeg
  27. BIN
      assets/images/test.jpeg
  28. 34 0
      ios/.gitignore
  29. 26 0
      ios/Flutter/AppFrameworkInfo.plist
  30. 2 0
      ios/Flutter/Debug.xcconfig
  31. 2 0
      ios/Flutter/Release.xcconfig
  32. 43 0
      ios/Podfile
  33. 616 0
      ios/Runner.xcodeproj/project.pbxproj
  34. 7 0
      ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  35. 8 0
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  36. 8 0
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  37. 101 0
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  38. 7 0
      ios/Runner.xcworkspace/contents.xcworkspacedata
  39. 8 0
      ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  40. 8 0
      ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  41. 13 0
      ios/Runner/AppDelegate.swift
  42. 122 0
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  43. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  44. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  45. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  46. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  47. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  48. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  49. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  50. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  51. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  52. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  53. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  54. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  55. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  56. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  57. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  58. 23 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  59. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  60. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  61. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  62. 5 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  63. 37 0
      ios/Runner/Base.lproj/LaunchScreen.storyboard
  64. 26 0
      ios/Runner/Base.lproj/Main.storyboard
  65. 49 0
      ios/Runner/Info.plist
  66. 1 0
      ios/Runner/Runner-Bridging-Header.h
  67. 12 0
      ios/RunnerTests/RunnerTests.swift
  68. 64 0
      lib/app_lifecycle/app_lifecycle.dart
  69. 16 0
      lib/config/config.dart
  70. 118 0
      lib/config/device.dart
  71. 213 0
      lib/main.dart
  72. 372 0
      lib/play/board.dart
  73. 140 0
      lib/play/board_painter.dart
  74. 575 0
      lib/play/board_play.dart
  75. 575 0
      lib/play/piece.dart
  76. 1 0
      linux/.gitignore
  77. 128 0
      linux/CMakeLists.txt
  78. 88 0
      linux/flutter/CMakeLists.txt
  79. 15 0
      linux/flutter/generated_plugin_registrant.cc
  80. 15 0
      linux/flutter/generated_plugin_registrant.h
  81. 24 0
      linux/flutter/generated_plugins.cmake
  82. 26 0
      linux/runner/CMakeLists.txt
  83. 6 0
      linux/runner/main.cc
  84. 130 0
      linux/runner/my_application.cc
  85. 18 0
      linux/runner/my_application.h
  86. 7 0
      macos/.gitignore
  87. 2 0
      macos/Flutter/Flutter-Debug.xcconfig
  88. 2 0
      macos/Flutter/Flutter-Release.xcconfig
  89. 16 0
      macos/Flutter/GeneratedPluginRegistrant.swift
  90. 42 0
      macos/Podfile
  91. 35 0
      macos/Podfile.lock
  92. 801 0
      macos/Runner.xcodeproj/project.pbxproj
  93. 8 0
      macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  94. 99 0
      macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  95. 10 0
      macos/Runner.xcworkspace/contents.xcworkspacedata
  96. 8 0
      macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  97. 13 0
      macos/Runner/AppDelegate.swift
  98. 68 0
      macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  99. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
  100. BIN
      macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png

+ 45 - 0
.gitignore

@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release

+ 45 - 0
.metadata

@@ -0,0 +1,45 @@
+# 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: "edada7c56edf4a183c1735310e123c7f923584f1"
+  channel: "[user-branch]"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+      base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+    - platform: android
+      create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+      base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+    - platform: ios
+      create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+      base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+    - platform: linux
+      create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+      base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+    - platform: macos
+      create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+      base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+    - platform: web
+      create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+      base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+    - platform: windows
+      create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+      base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'

+ 25 - 0
.vscode/launch.json

@@ -0,0 +1,25 @@
+{
+  // Use IntelliSense to learn about possible attributes.
+  // Hover to view descriptions of existing attributes.
+  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "image_puzzle",
+      "request": "launch",
+      "type": "dart"
+    },
+    {
+      "name": "image_puzzle (profile mode)",
+      "request": "launch",
+      "type": "dart",
+      "flutterMode": "profile"
+    },
+    {
+      "name": "image_puzzle (release mode)",
+      "request": "launch",
+      "type": "dart",
+      "flutterMode": "release"
+    }
+  ]
+}

+ 16 - 0
README.md

@@ -0,0 +1,16 @@
+# puzzleweave
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.

+ 28 - 0
analysis_options.yaml

@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+  # The lint rules applied to this project can be customized in the
+  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+  # included above or to enable additional rules. A list of all available lints
+  # and their documentation is published at https://dart.dev/lints.
+  #
+  # Instead of disabling a lint rule for the entire project in the
+  # section below, it can also be suppressed for a single line of code
+  # or a specific dart file by using the `// ignore: name_of_lint` and
+  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+  # producing the lint.
+  rules:
+    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
+    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options

+ 14 - 0
android/.gitignore

@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks

+ 44 - 0
android/app/build.gradle.kts

@@ -0,0 +1,44 @@
+plugins {
+    id("com.android.application")
+    id("kotlin-android")
+    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+    id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+    namespace = "com.example.image_puzzle"
+    compileSdk = flutter.compileSdkVersion
+    ndkVersion = flutter.ndkVersion
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_11
+        targetCompatibility = JavaVersion.VERSION_11
+    }
+
+    kotlinOptions {
+        jvmTarget = JavaVersion.VERSION_11.toString()
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId = "com.example.image_puzzle"
+        // You can update the following values to match your application needs.
+        // For more information, see: https://flutter.dev/to/review-gradle-config.
+        minSdk = flutter.minSdkVersion
+        targetSdk = flutter.targetSdkVersion
+        versionCode = flutter.versionCode
+        versionName = flutter.versionName
+    }
+
+    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.getByName("debug")
+        }
+    }
+}
+
+flutter {
+    source = "../.."
+}

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

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- The INTERNET permission is required for development. Specifically,
+         the Flutter tool 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>

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

@@ -0,0 +1,45 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application
+        android:label="image_puzzle"
+        android:name="${applicationName}"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:launchMode="singleTop"
+            android:taskAffinity=""
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.NormalTheme"
+              android:resource="@style/NormalTheme"
+              />
+            <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>
+    <!-- Required to query activities that can process text, see:
+         https://developer.android.com/training/package-visibility and
+         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
+
+         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.PROCESS_TEXT"/>
+            <data android:mimeType="text/plain"/>
+        </intent>
+    </queries>
+</manifest>

+ 5 - 0
android/app/src/main/kotlin/com/example/image_puzzle/MainActivity.kt

@@ -0,0 +1,5 @@
+package com.example.image_puzzle
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()

+ 12 - 0
android/app/src/main/res/drawable-v21/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:colorBackground" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>

+ 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>

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


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


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


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


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


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

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             the Flutter engine draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

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

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             the Flutter engine draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</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">
+    <!-- The INTERNET permission is required for development. Specifically,
+         the Flutter tool 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>

+ 21 - 0
android/build.gradle.kts

@@ -0,0 +1,21 @@
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+    project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+    project.evaluationDependsOn(":app")
+}
+
+tasks.register<Delete>("clean") {
+    delete(rootProject.layout.buildDirectory)
+}

+ 3 - 0
android/gradle.properties

@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true

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

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

+ 25 - 0
android/settings.gradle.kts

@@ -0,0 +1,25 @@
+pluginManagement {
+    val flutterSdkPath = run {
+        val properties = java.util.Properties()
+        file("local.properties").inputStream().use { properties.load(it) }
+        val flutterSdkPath = properties.getProperty("flutter.sdk")
+        require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+        flutterSdkPath
+    }
+
+    includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+    repositories {
+        google()
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+
+plugins {
+    id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+    id("com.android.application") version "8.7.3" apply false
+    id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")

BIN
assets/icons/test.jpg


BIN
assets/images/opt.jpeg


BIN
assets/images/test.jpeg


+ 34 - 0
ios/.gitignore

@@ -0,0 +1,34 @@
+**/dgph
+*.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/ephemeral/
+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>en</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>12.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"

+ 43 - 0
ios/Podfile

@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '12.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 flutter_root
+  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+  unless File.exist?(generated_xcode_build_settings_path)
+    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+  end
+
+  File.foreach(generated_xcode_build_settings_path) do |line|
+    matches = line.match(/FLUTTER_ROOT\=(.*)/)
+    return matches[1].strip if matches
+  end
+  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+  use_frameworks!
+
+  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+  target 'RunnerTests' do
+    inherit! :search_paths
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_ios_build_settings(target)
+  end
+end

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

@@ -0,0 +1,616 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 54;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		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 PBXContainerItemProxy section */
+		331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+			remoteInfo = Runner;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			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>"; };
+		331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
+		331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; 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>"; };
+		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>"; };
+		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 = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		331C8082294A63A400263BE5 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				331C807B294A618700263BE5 /* RunnerTests.swift */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+				331C8082294A63A400263BE5 /* RunnerTests */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+				331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		331C8080294A63A400263BE5 /* RunnerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+			buildPhases = (
+				331C807D294A63A400263BE5 /* Sources */,
+				331C807F294A63A400263BE5 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				331C8086294A63A400263BE5 /* PBXTargetDependency */,
+			);
+			name = RunnerTests;
+			productName = RunnerTests;
+			productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+			);
+			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 = {
+				BuildIndependentTargetsInParallel = YES;
+				LastUpgradeCheck = 1510;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					331C8080294A63A400263BE5 = {
+						CreatedOnToolsVersion = 14.0;
+						TestTargetID = 97C146ED1CF9000F007C117D;
+					};
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						LastSwiftMigration = 1100;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+				331C8080294A63A400263BE5 /* RunnerTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		331C807F294A63A400263BE5 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		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;
+			alwaysOutOfDate = 1;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+			);
+			name = "Thin Binary";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+		};
+		9740EEB61CF901F6004384FC /* Run Script */ = {
+			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
+			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";
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		331C807D294A63A400263BE5 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 97C146ED1CF9000F007C117D /* Runner */;
+			targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency 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;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				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;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				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 = 12.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;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		331C8088294A63A400263BE5 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+			};
+			name = Debug;
+		};
+		331C8089294A63A400263BE5 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+			};
+			name = Release;
+		};
+		331C808A294A63A400263BE5 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				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;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				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 = 12.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				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;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				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 = 12.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				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;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle;
+				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;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle;
+				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 */
+		331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				331C8088294A63A400263BE5 /* Debug */,
+				331C8089294A63A400263BE5 /* Release */,
+				331C808A294A63A400263BE5 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		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 = "self:">
+   </FileRef>
+</Workspace>

+ 8 - 0
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 8 - 0
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@@ -0,0 +1,8 @@
+<?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>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>

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

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1510"
+   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"
+      customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "331C8080294A63A400263BE5"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      enableGPUValidationMode = "1"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </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>

+ 7 - 0
ios/Runner.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>

+ 8 - 0
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 8 - 0
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@@ -0,0 +1,8 @@
+<?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>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>

+ 13 - 0
ios/Runner/AppDelegate.swift

@@ -0,0 +1,13 @@
+import Flutter
+import UIKit
+
+@main
+@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>

+ 49 - 0
ios/Runner/Info.plist

@@ -0,0 +1,49 @@
+<?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>CFBundleDisplayName</key>
+	<string>Image Puzzle</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>image_puzzle</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>CADisableMinimumFrameDurationOnPhone</key>
+	<true/>
+	<key>UIApplicationSupportsIndirectInputEvents</key>
+	<true/>
+</dict>
+</plist>

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

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

+ 12 - 0
ios/RunnerTests/RunnerTests.swift

@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+  func testExample() {
+    // If you add code to the Runner application, consider adding tests here.
+    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+  }
+
+}

+ 64 - 0
lib/app_lifecycle/app_lifecycle.dart

@@ -0,0 +1,64 @@
+// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+import 'package:logging/logging.dart';
+import 'package:provider/provider.dart';
+
+class AppLifecycleObserver extends StatefulWidget {
+  final Widget child;
+
+  const AppLifecycleObserver({required this.child, super.key});
+
+  @override
+  State<AppLifecycleObserver> createState() => _AppLifecycleObserverState();
+}
+
+class _AppLifecycleObserverState extends State<AppLifecycleObserver> with WidgetsBindingObserver {
+  static final _log = Logger('AppLifecycleObserver');
+
+  final ValueNotifier<AppLifecycleState> lifecycleListenable = ValueNotifier(AppLifecycleState.resumed); // default set to AppLifecycleState.resumed
+
+  @override
+  Widget build(BuildContext context) {
+    // Using InheritedProvider because we don't want to use Consumer
+    // or context.watch or anything like that to listen to this. We want
+    // to manually add listeners. We're interested in the _events_ of lifecycle
+    // state changes, and not so much in the state itself. (For example,
+    // we want to stop sound when the app goes into the background, and
+    // restart sound again when the app goes back into focus. We're not
+    // rebuilding any widgets.)
+    //
+    // Provider, by default, throws when one
+    // is trying to provide a Listenable (such as ValueNotifier) without using
+    // something like ValueListenableProvider. InheritedProvider is more
+    // low-level and doesn't have this problem.
+    return InheritedProvider<ValueNotifier<AppLifecycleState>>.value(value: lifecycleListenable, child: widget.child);
+  }
+
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    _log.info(() => 'didChangeAppLifecycleState: $state');
+    lifecycleListenable.value = state;
+  }
+
+  @override
+  void didHaveMemoryPressure() {
+    _log.warning(() => 'didHaveMemoryPressure');
+    // FirebaseCrashlytics.instance.log("didHaveMemoryPressure");
+  }
+
+  @override
+  void dispose() {
+    WidgetsBinding.instance.removeObserver(this);
+    super.dispose();
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    WidgetsBinding.instance.addObserver(this);
+    _log.info('Subscribed to app lifecycle updates');
+  }
+}

+ 16 - 0
lib/config/config.dart

@@ -0,0 +1,16 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+
+import 'device.dart';
+
+class Config {
+  static bool get isDebug => true;
+
+  late Device device;
+
+  Config(
+    BuildContext context,
+    Directory baseDir,
+  ) : device = Device(context, baseDir);
+}

+ 118 - 0
lib/config/device.dart

@@ -0,0 +1,118 @@
+import 'dart:io';
+import 'dart:math';
+import 'dart:ui';
+
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+
+class Device {
+  final Logger _log = Logger('Device');
+
+  final Directory baseDir;
+
+  Device(this.context, this.baseDir);
+
+  AndroidDeviceInfo? androidDeviceInfo;
+
+  /// 获取平台性能
+  int get androidSdkInt => androidDeviceInfo != null ? androidDeviceInfo!.version.sdkInt : 100;
+  bool get isOldAndroid => androidSdkInt < 26; // 安卓8以下
+  bool get isLowRamDevice => androidDeviceInfo != null ? androidDeviceInfo!.isLowRamDevice : false;
+  bool get isLowEndDevice => Platform.numberOfProcessors <= 1 || isOldAndroid || isLowRamDevice;
+
+  static double get devPixelRatio => PlatformDispatcher.instance.views.first.devicePixelRatio;
+  static Size get physicalSize => PlatformDispatcher.instance.views.first.physicalSize;
+  static Size get logicalSize => physicalSize / devPixelRatio;
+
+  ///final Size screenSize;
+  final BuildContext context;
+
+  final aspectRatio = 3.0 / 2.0; // 图片宽高比按照 2 / 3 设定,相应的 board play 区域也是这个比例
+
+  final _tabletProfile = DeviceProfile(horizontalPadding: 20, verticalPadding: 10);
+
+  final _profile = DeviceProfile(horizontalPadding: 10, verticalPadding: 10);
+
+  double _bannerHeight = 0;
+
+  int get previewImageSize => (boardSize.shortestSide * devicePixelRatio).toInt();
+
+  set bannerHeight(double height) {
+    _bannerHeight = height;
+  }
+
+  /// 获取当前配置
+  DeviceProfile get profile => isTablet ? _tabletProfile : _profile;
+
+  /// 屏幕尺寸
+  Size get screenSize => MediaQuery.of(context).size;
+
+  /// 像素密度
+  double get devicePixelRatio => isLowEndDevice ? 1 : MediaQuery.of(context).devicePixelRatio;
+  double get dpi => devicePixelRatio * 160;
+
+  /// safeArea高度 Z
+  double get safeAreaHeight => MediaQuery.of(context).viewPadding.top + MediaQuery.of(context).viewPadding.bottom;
+
+  /// 获取appbar的高度
+  double get appBarHeight => AppBar().preferredSize.height;
+
+  /// Play 尺寸
+  Size get boardSize =>
+      Size(screenSize.width - profile.horizontalPadding * 2, screenSize.height - (safeAreaHeight + appBarHeight + profile.verticalPadding * 2));
+
+  /// 根据谷歌的描述,banner尺寸最小50px, 最大不超过15%
+  double get estimateBannerHeight => max(50, screenSize.height * 0.15).truncateToDouble();
+  double get bannerHeight => _bannerHeight == 0 ? estimateBannerHeight : _bannerHeight;
+
+  /// 是否平板
+  bool get isTablet => screenSize.shortestSide >= 600;
+
+  String filePath(String relativePath) => '${baseDir.path}/$relativePath';
+
+  // board核心绘制区域
+  Rect get targetRect {
+    final double appBarHeight = AppBar().preferredSize.height + MediaQuery.of(context).padding.top;
+    const double bannerHeight = 50.0;
+
+    final double availableHeight = screenSize.height - appBarHeight - bannerHeight;
+
+    final double paddedWidth = screenSize.width - 2 * profile.horizontalPadding;
+    final double paddedHeight = availableHeight - 2 * profile.verticalPadding;
+
+    final double targetWidth = paddedWidth;
+    final double targetHeight = targetWidth * aspectRatio;
+
+    final double finalPuzzleWidth;
+    final double finalPuzzleHeight;
+
+    if (targetHeight > paddedHeight) {
+      finalPuzzleHeight = paddedHeight;
+      finalPuzzleWidth = paddedHeight / aspectRatio;
+    } else {
+      finalPuzzleWidth = targetWidth;
+      finalPuzzleHeight = targetHeight;
+    }
+
+    final double targetYStart = appBarHeight + profile.verticalPadding + (paddedHeight - finalPuzzleHeight) / 2;
+    final double targetXStart = profile.horizontalPadding + (paddedWidth - finalPuzzleWidth) / 2;
+
+    final newTargetRect = Rect.fromLTWH(targetXStart, targetYStart, finalPuzzleWidth, finalPuzzleHeight);
+
+    return newTargetRect;
+  }
+
+  // 最佳图片分辨率
+  Size get bestImageSize => Size(targetRect.width * devPixelRatio, targetRect.height * devPixelRatio);
+}
+
+class DeviceProfile {
+  final double horizontalPadding; // 水平padding
+  final double verticalPadding; // 垂直padding
+
+  DeviceProfile({
+    this.horizontalPadding = 10,
+    this.verticalPadding = 10, //Play board padding
+  });
+}

+ 213 - 0
lib/main.dart

@@ -0,0 +1,213 @@
+import 'dart:io';
+
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:image_puzzle/app_lifecycle/app_lifecycle.dart';
+import 'package:image_puzzle/play/board_play.dart';
+import 'package:logging/logging.dart';
+import 'dart:developer' as dev;
+
+import 'package:path_provider/path_provider.dart';
+import 'package:provider/provider.dart';
+
+import 'config/config.dart';
+import 'config/device.dart';
+
+Logger _log = Logger('main.dart');
+
+final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
+
+void main() async {
+  // Subscribe to log messages.
+  Logger.root.onRecord.listen((record) {
+    dev.log(
+      record.message,
+      time: record.time,
+      level: record.level.value,
+      name: record.loggerName,
+      zone: record.zone,
+      error: record.error,
+      stackTrace: record.stackTrace,
+    );
+  });
+
+  WidgetsFlutterBinding.ensureInitialized();
+
+  // 强制竖屏
+  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
+
+  // 进入全屏沉浸式, 隐藏底部导航以及状态栏
+  if (Platform.isAndroid) {
+    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
+  }
+
+  SystemChrome.setSystemUIOverlayStyle(
+    const SystemUiOverlayStyle(
+      statusBarColor: Colors.transparent, // <-- SEE HERE
+      statusBarIconBrightness: Brightness.dark, //<-- For Android SEE HERE (dark icons)
+      statusBarBrightness: Brightness.light, //<-- For iOS SEE HERE (dark icons)
+    ),
+  );
+
+  Directory baseDir = await getApplicationDocumentsDirectory();
+
+  runApp(MyApp(baseDir: baseDir));
+}
+
+class MyApp extends StatelessWidget {
+  final Directory baseDir;
+  const MyApp({super.key, required this.baseDir});
+
+  // This widget is the root of your application.
+  @override
+  Widget build(BuildContext context) {
+    Config config = Config(context, baseDir);
+    return AppLifecycleObserver(
+      child: MultiProvider(
+        providers: [
+          Provider<Config>(lazy: false, create: (context) => config),
+          Provider<Device>(lazy: false, create: (context) => config.device),
+        ],
+        child: Prepare(
+          child: MaterialApp(
+            key: GlobalKey(),
+            title: 'Image Puzzle',
+            initialRoute: '/play',
+            navigatorObservers: [routeObserver],
+            routes: {
+              '/': (context) => const MyHomePage(title: "Image Puzzle"),
+              '/play': (context) => BoardPlay(cols: 4, rows: 4),
+            },
+            theme: ThemeData(brightness: Brightness.light, primaryColor: Colors.green, primarySwatch: Colors.blue),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class MyHomePage extends StatefulWidget {
+  const MyHomePage({super.key, required this.title});
+
+  // This widget is the home page of your application. It is stateful, meaning
+  // that it has a State object (defined below) that contains fields that affect
+  // how it looks.
+
+  // This class is the configuration for the state. It holds the values (in this
+  // case the title) provided by the parent (in this case the App widget) and
+  // used by the build method of the State. Fields in a Widget subclass are
+  // always marked "final".
+
+  final String title;
+
+  @override
+  State<MyHomePage> createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  int _counter = 0;
+
+  void _incrementCounter() {
+    setState(() {
+      // This call to setState tells the Flutter framework that something has
+      // changed in this State, which causes it to rerun the build method below
+      // so that the display can reflect the updated values. If we changed
+      // _counter without calling setState(), then the build method would not be
+      // called again, and so nothing would appear to happen.
+      _counter++;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // This method is rerun every time setState is called, for instance as done
+    // by the _incrementCounter method above.
+    //
+    // The Flutter framework has been optimized to make rerunning build methods
+    // fast, so that you can just rebuild anything that needs updating rather
+    // than having to individually change instances of widgets.
+    return Scaffold(
+      appBar: AppBar(
+        // TRY THIS: Try changing the color here to a specific color (to
+        // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
+        // change color while the other colors stay the same.
+        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
+        // Here we take the value from the MyHomePage object that was created by
+        // the App.build method, and use it to set our appbar title.
+        title: Text(widget.title),
+      ),
+      body: Center(
+        // Center is a layout widget. It takes a single child and positions it
+        // in the middle of the parent.
+        child: Column(
+          // Column is also a layout widget. It takes a list of children and
+          // arranges them vertically. By default, it sizes itself to fit its
+          // children horizontally, and tries to be as tall as its parent.
+          //
+          // Column has various properties to control how it sizes itself and
+          // how it positions its children. Here we use mainAxisAlignment to
+          // center the children vertically; the main axis here is the vertical
+          // axis because Columns are vertical (the cross axis would be
+          // horizontal).
+          //
+          // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
+          // action in the IDE, or press "p" in the console), to see the
+          // wireframe for each widget.
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            const Text('You have pushed the button this many times:'),
+            Text('$_counter', style: Theme.of(context).textTheme.headlineMedium),
+          ],
+        ),
+      ),
+      floatingActionButton: FloatingActionButton(
+        onPressed: _incrementCounter,
+        tooltip: 'Increment',
+        child: const Icon(Icons.add),
+      ), // This trailing comma makes auto-formatting nicer for build methods.
+    );
+  }
+}
+
+class Prepare extends StatefulWidget {
+  final Widget child;
+  const Prepare({super.key, required this.child});
+
+  @override
+  State<Prepare> createState() => _PrepareState();
+}
+
+class _PrepareState extends State<Prepare> {
+  @override
+  void initState() {
+    super.initState();
+    loadDeviceInfo();
+  }
+
+  /// 获取android平台信息,用户判断是否低端机
+  loadDeviceInfo() async {
+    DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
+    if (Platform.isAndroid) {
+      try {
+        context.read<Device>().androidDeviceInfo = await deviceInfoPlugin.androidInfo;
+      } catch (e) {}
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    //Update ad banner size
+    // AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
+    //   MediaQuery.of(context).size.width.truncate(),
+    // ).then((value) {
+    //   if (value != null) {
+    //     context.read<Device>().bannerHeight = value.height.toDouble();
+    //   }
+    // }).catchError((err) {
+    //   //todo
+    // });
+    context.read<Device>().bannerHeight = 50;
+    return widget.child;
+  }
+}

+ 372 - 0
lib/play/board.dart

@@ -0,0 +1,372 @@
+// board.dart
+
+import 'dart:async';
+import 'dart:math';
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+import 'package:flutter/material.dart';
+import 'package:image_puzzle/config/device.dart';
+import 'package:image_puzzle/play/piece.dart';
+import 'package:logging/logging.dart';
+import 'package:vector_math/vector_math.dart' as vmath;
+
+final Logger _log = Logger('board.dart');
+
+enum BoardStatus { loading, playing, success }
+
+class Board {
+  // 原图
+  final ui.Image image;
+
+  // 所有拼图碎片
+  final List<Piece> pieces = [];
+
+  /// 拼图行数(3/4/5,对应9/16/25宫格)
+  final int rows;
+
+  /// 拼图列数(3/4/5)
+  final int cols;
+
+  /// 整个拼图在屏幕上的目标区域(最终完整显示的位置和大小)
+  final Rect targetRect;
+
+  // 碎片的逻辑宽高
+  double get pieceLogicalWidth => targetRect.width / cols;
+  double get pieceLogicalHeight => targetRect.height / rows;
+
+  // 设备信息
+  final Device device;
+
+  // 静态背景绘图 Picture (只绘制一次)
+  ui.Picture? _backgroundPicture;
+  ui.Picture? get backgroundPicture => _backgroundPicture;
+
+  ValueNotifier boardNotifier = ValueNotifier(1);
+
+  // 用户是否真正可以开始动手开玩(所有加载初始化完成,并且进入的插屏广告播放完成)
+  final Completer<bool> startPlayCompleter = Completer();
+
+  BoardStatus _status = BoardStatus.loading;
+  BoardStatus get status => _status;
+
+  // 备份的 groups (PieceGroup 对象的列表)
+  List<PieceGroup> backupGroups = [];
+
+  void start() {
+    _status = BoardStatus.playing;
+    invalidate();
+  }
+
+  void success() {
+    _status = BoardStatus.success;
+    invalidate();
+  }
+
+  void invalidate() {
+    boardNotifier.value++;
+  }
+
+  // 是否全部完成
+  bool get isAllDone => pieces.every((p) => p.isOK);
+
+  final TickerProviderStateMixin ticker;
+
+  Board(this.ticker, this.image, this.cols, this.rows, this.targetRect, this.device, {Map<String, dynamic>? json}) {
+    _recordBackground(); // 录制静态背景,提升性能
+    _initPieces();
+    rebuildAllGroups();
+    backupAllGroups();
+  }
+
+  /// 初始化碎片
+  _initPieces() {
+    final double imagePixelWidth = image.width.toDouble(); // 图片像素宽度
+    final double imagePixelHeight = image.height.toDouble(); // 图片像素高度
+    final double pieceLogicalWidth = targetRect.width / cols; // 绘图区域逻辑宽度 / 列数 = 每个piece的逻辑宽度
+    final double pieceLogicalHeight = targetRect.height / rows; // 绘图区域逻辑高度 / 行数 = 每个piece的逻辑高度
+    final double scaleX = imagePixelWidth / targetRect.width; // 像素宽度与逻辑宽度的scale比例
+    final double scaleY = imagePixelHeight / targetRect.height; // 像素高度与逻辑高度的scale比例
+    final double piecePixelWidth = pieceLogicalWidth * scaleX; // 每个piece的像素宽度
+    final double piecePixelHeight = pieceLogicalHeight * scaleY; // 每个piece的像素高度
+
+    final List<({int col, int row})> slotCoords = [];
+
+    for (int r = 0; r < rows; r++) {
+      for (int c = 0; c < cols; c++) {
+        slotCoords.add((col: c, row: r));
+      }
+    }
+
+    slotCoords.shuffle(Random());
+
+    int index = 0;
+
+    for (int r = 0; r < rows; r++) {
+      for (int c = 0; c < cols; c++) {
+        final correctX = targetRect.left + c * pieceLogicalWidth; // piece的正确位置坐标 x
+        final correctY = targetRect.top + r * pieceLogicalHeight; // piece的正确位置坐标 y
+        final correctOffset = Offset(correctX, correctY);
+
+        final initialSlot = slotCoords[index];
+        final initialX = targetRect.left + initialSlot.col * pieceLogicalWidth; // 初始位置坐标
+        final initialY = targetRect.top + initialSlot.row * pieceLogicalHeight;
+
+        final initialTransform = vmath.Matrix4.translationValues(initialX, initialY, 0.0); // 记录初始位置坐标transform matrix
+
+        final sourceRect = Rect.fromLTWH(c * piecePixelWidth, r * piecePixelHeight, piecePixelWidth, piecePixelHeight); // 对应图片的像素矩形
+
+        pieces.add(
+          Piece(
+            board: this,
+            index: index,
+            row: r,
+            col: c,
+            rows: rows,
+            cols: cols,
+            correctOffset: correctOffset,
+            curCol: initialSlot.col,
+            curRow: initialSlot.row,
+            sourceRect: sourceRect,
+            transform: initialTransform,
+            borders: [true, true, true, true],
+          ),
+        );
+        index++;
+      }
+    }
+  }
+
+  // 根据坐标查找指定位置的碎片
+  Piece? findPieceAt(Offset localPos) {
+    for (var piece in pieces.reversed) {
+      // 从上层开始找
+      // 计算碎片在画布上的绝对位置board
+      final transform = piece.transform;
+      final posX = transform.storage[12];
+      final posY = transform.storage[13];
+      final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
+
+      if (pieceRect.contains(localPos)) {
+        return piece;
+      }
+    }
+    return null;
+  }
+
+  // 查找某个坐标上的碎片,排除某个piece
+  Piece? findPieceAtExclude(Offset localPos, Piece excludePiece) {
+    for (var piece in pieces.reversed) {
+      // 从上层开始找
+      if (piece == excludePiece) continue;
+      // 计算碎片在画布上的绝对位置
+      final transform = piece.transform;
+      final posX = transform.storage[12];
+      final posY = transform.storage[13];
+      final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
+
+      if (pieceRect.contains(localPos)) {
+        return piece;
+      }
+    }
+    return null;
+  }
+
+  vmath.Matrix4 getTransformByCoordinate(int row, int col) {
+    final x = targetRect.left + col * pieceLogicalWidth;
+    final y = targetRect.top + row * pieceLogicalHeight;
+
+    final transform = vmath.Matrix4.translationValues(x, y, 0.0);
+    return transform;
+  }
+
+  // 根据坐标获取该位置上的piece
+  Piece? getPieceByCoordinate(int row, int col) {
+    return pieces.firstWhereOrNull((p) => p.curRow == row && p.curCol == col);
+  }
+
+  // 根据坐标获取该位置上的piece
+  Piece? getPieceByIndex(int index) {
+    return pieces.firstWhereOrNull((p) => p.index == index);
+  }
+
+  // 初始化检查合并分组 (改进版:固定点迭代合并)
+  void rebuildAllGroups() {
+    _log.info('rebuildAllGroups');
+    // 1. 清除所有旧组 和 原path
+    for (var p in pieces) {
+      p.group = null;
+      p.path = null;
+      p.outLinePath = null;
+      p.innerLinePath = null;
+    }
+
+    bool mergedInPass;
+
+    // 2. 迭代合并,直到一轮循环中没有发生任何合并
+    do {
+      mergedInPass = false;
+
+      // 3. 遍历所有碎片对 (i, j)
+      for (int i = 0; i < pieces.length; i++) {
+        for (int j = i + 1; j < pieces.length; j++) {
+          final piece = pieces[i];
+          final otherPiece = pieces[j];
+
+          // 4. 检查是否可以合并,并且它们不属于同一个组
+          if (piece.canMerge(otherPiece)) {
+            if (!piece.isSameGroup(otherPiece)) {
+              // 5. 合并组
+              // piece.groupWith(otherPiece) 会创建一个新的 PieceGroup,
+              // 并将 piece 和 otherPiece (以及它们可能已有的组员) 的 group 引用全部指向新组。
+              piece.groupWith(otherPiece);
+              mergedInPass = true;
+            }
+          }
+        }
+      }
+      // 如果 mergedInPass 为 true,说明本轮循环发生了合并,需要重新开始下一轮遍历
+      // 因为新的合并可能促使其他碎片或碎片组也得以连接
+    } while (mergedInPass);
+  }
+
+  // 备份所有group, 方便进行比较, 发现新合成的group,以便呈现动画特效
+  void backupAllGroups() {
+    backupGroups.clear();
+    for (var p in pieces) {
+      if (p.group != null && !backupGroups.contains(p.group)) {
+        backupGroups.add(p.group!);
+      }
+    }
+    _log.info('backupAllGroups: ${backupGroups.length}');
+  }
+
+  // 当前group与之前备份的group进行比较,返回新合并的group列表,这些group需要展示动画特效
+  List<PieceGroup> compareAllGroups() {
+    _log.info('compareAllGroups');
+    List<PieceGroup> newGroups = [];
+
+    List<PieceGroup> currentGroups = [];
+    for (var p in pieces) {
+      if (p.group != null && !currentGroups.contains(p.group)) {
+        currentGroups.add(p.group!);
+      }
+    }
+
+    if (backupGroups.isEmpty) {
+      // 之前都不存在群组,那么当前新合成的所有group都是新group
+      newGroups.addAll(currentGroups);
+    } else {
+      for (var g in currentGroups) {
+        bool alreadyExists = false;
+        for (var bakgroup in backupGroups) {
+          if (bakgroup.containsGroup(g)) {
+            alreadyExists = true;
+            break;
+          }
+        }
+        if (!alreadyExists) {
+          newGroups.add(g);
+        }
+      }
+    }
+
+    if (newGroups.isNotEmpty) {
+      for (var g in newGroups) {
+        _log.info('发现新群组:');
+        g.print();
+      }
+    }
+
+    return newGroups;
+  }
+
+  // 检查游戏是否全部完成
+  bool checkWinCondition() {
+    return pieces.every((p) => p.isOK);
+  }
+
+  /// 退出释放资源
+  dispose() {
+    _backgroundPicture?.dispose();
+    _backgroundPicture = null;
+    image.dispose();
+    boardNotifier.dispose();
+  }
+
+  // 录制背景,避免每次都重复绘制,提升性能
+  void _recordBackground() {
+    // 确保在录制前释放旧的 Picture 资源
+    _backgroundPicture?.dispose();
+    _backgroundPicture = null;
+
+    final recorder = ui.PictureRecorder();
+
+    // 录制器的边界设置为整个屏幕尺寸,因为我们在这里绘制的是 full screen background。
+    final recordBounds = Rect.fromLTWH(0, 0, device.screenSize.width, device.screenSize.height);
+
+    final canvas = Canvas(recorder, recordBounds);
+
+    // --- 静态绘制配置 ---
+    const double cornerRadius = 8.0;
+    const double strokeWidth = 1.0; // 拼图槽位的线宽
+    final double halfStroke = strokeWidth / 2.0;
+
+    // 1. 绘制整个屏幕背景
+    canvas.drawRect(
+      recordBounds,
+      Paint()
+        ..color = Colors
+            .lightGreen // 主背景色
+        ..style = PaintingStyle.fill,
+    );
+
+    // 2. 绘制每个拼图槽位(圆角矩形)作为背景的一部分 (静态内容)
+    final slotFillPaint = Paint()
+      ..color = Colors.green
+      // .shade100 // 槽位填充色
+      ..style = PaintingStyle.fill;
+
+    final slotStrokePaint = Paint()
+      ..color =
+          Color(0xff26600c) // 槽位边框色
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = strokeWidth;
+
+    for (int r = 0; r < rows; r++) {
+      for (int c = 0; c < cols; c++) {
+        // 计算当前槽位的边界 (Canvas坐标系)
+        final left = targetRect.left + c * pieceLogicalWidth;
+        final top = targetRect.top + r * pieceLogicalHeight;
+        final right = left + pieceLogicalWidth;
+        final bottom = top + pieceLogicalHeight;
+
+        // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke
+        final slotRect = Rect.fromLTRB(left + halfStroke, top + halfStroke, right - halfStroke, bottom - halfStroke);
+
+        final slotRRect = RRect.fromRectAndRadius(slotRect, const Radius.circular(cornerRadius));
+
+        // 绘制填充
+        canvas.drawRRect(slotRRect, slotFillPaint);
+
+        // 绘制描边
+        canvas.drawRRect(slotRRect, slotStrokePaint);
+      }
+    }
+
+    // --- 结束录制并存储 ---
+    _backgroundPicture = recorder.endRecording();
+    _log.info('Static background picture recorded. Size: ${recordBounds.size}');
+  }
+}
+
+// 辅助扩展:解决 Dart 缺少 firstWhereOrNull 的问题
+extension IterableExtension<T> on Iterable<T> {
+  T? firstWhereOrNull(bool Function(T element) test) {
+    for (final element in this) {
+      if (test(element)) {
+        return element;
+      }
+    }
+    return null;
+  }
+}

+ 140 - 0
lib/play/board_painter.dart

@@ -0,0 +1,140 @@
+// board_painter.dart
+
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+
+import 'package:image_puzzle/play/board.dart';
+import 'package:image_puzzle/play/piece.dart';
+
+class BoardPainter extends CustomPainter {
+  final Board board;
+
+  // 碎片边框宽度 (可以比背景槽位稍粗,以便突出)
+  static const double _pieceStrokeWidth = 1.0;
+
+  // 边框画笔
+  final Paint _pieceBorderPaint = Paint()
+    ..color = Colors
+        .black // 碎片边框颜色
+    ..style = PaintingStyle.stroke
+    ..strokeWidth = _pieceStrokeWidth
+    ..isAntiAlias = true;
+
+  // 边框画笔
+  final Paint _innerPieceBorderPaint = Paint()
+    ..color = Colors
+        .white // 碎片边框颜色
+    ..style = PaintingStyle.stroke
+    ..strokeWidth = _pieceStrokeWidth
+    ..isAntiAlias = true;
+
+  BoardPainter({required this.board}) : super(repaint: Listenable.merge([board.boardNotifier])); // 触发重绘
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    // 绘制背景
+    // _paintBackground(canvas, size);
+    if (board.backgroundPicture != null) {
+      canvas.drawPicture(board.backgroundPicture!);
+    }
+
+    // 绘制pieces
+    for (final piece in board.pieces) {
+      _drawPiece(canvas, size, piece);
+    }
+  }
+
+  @override
+  bool shouldRepaint(covariant BoardPainter oldDelegate) {
+    return true;
+  }
+
+  void _drawPiece(Canvas canvas, Size size, Piece piece) {
+    final double w = board.pieceLogicalWidth;
+    final double h = board.pieceLogicalHeight;
+
+    // 1. 准备变换矩阵和动态 Path
+    final Float64List storage64 = Float64List.fromList(piece.transform.storage);
+    // 核心:根据 borders 状态动态生成 Path
+    final List<Path> paths = _getPiecePath(piece);
+    final Path piecePath = paths[0];
+    final Path outerBorderPath = paths[1];
+    final Path innerBorderPath = paths[2];
+
+    canvas.save();
+
+    canvas.transform(storage64); // 应用平移
+
+    // 裁剪区域:使用动态生成的 Path 来裁剪图片,实现圆角/尖角混合
+    canvas.clipPath(piecePath);
+
+    // 绘制图片:只有落在 rrect 内部的部分图片会被绘制
+    final Rect dstRect = Rect.fromLTWH(0, 0, w, h);
+    canvas.drawImageRect(board.image, piece.sourceRect, dstRect, Paint()..isAntiAlias = true);
+
+    canvas.restore(); // 恢复 Canvas 状态 (移除裁剪和 transform)
+
+    // --- 绘制边框 ---
+    // 为了确保边框线宽向外延伸的部分不会被裁剪,我们需要在裁剪区域被移除后重新绘制。
+    canvas.save();
+    canvas.transform(storage64); // 重新应用平移
+
+    // 绘制碎片边框
+    canvas.drawPath(outerBorderPath, _pieceBorderPaint); // 外边框,黑色
+    canvas.drawPath(innerBorderPath, _innerPieceBorderPaint); // 内边框,白色
+
+    canvas.restore();
+  }
+
+  List<Path> _getPiecePath(Piece piece) {
+    if (piece.path == null || piece.outLinePath == null || piece.innerLinePath == null) {
+      return piece.generatePaths();
+    } else {
+      return [piece.path!, piece.outLinePath!, piece.innerLinePath!];
+    }
+  }
+
+  // 绘制背景(已经被drawPicture取代)
+  void _paintBackground(Canvas canvas, Size size) {
+    // 绘制整个背景
+    canvas.drawRect(
+      Rect.fromLTWH(0, 0, size.width, size.height),
+      Paint()
+        ..color = Colors.lightGreen
+        ..style = PaintingStyle.fill,
+    );
+
+    for (final piece in board.pieces) {
+      canvas.save();
+
+      // 转换 Matrix4 存储格式 (Float32List -> Float64List)
+      final Float64List storage64 = Float64List.fromList(piece.transform.storage);
+
+      // 应用碎片的几何变换 (平移/旋转/缩放)
+      // 这将 Canvas 原点移动到 piece.transform.storage[12] 和 [13] 处。
+      canvas.transform(storage64);
+
+      final Rect rect = Rect.fromLTWH(0, 0, board.pieceLogicalWidth, board.pieceLogicalHeight);
+
+      final RRect rrect = RRect.fromRectAndRadius(rect, Radius.circular(8.0));
+
+      canvas.drawRRect(
+        rrect,
+        Paint()
+          ..color = Colors.green
+          ..style = PaintingStyle.fill,
+      );
+
+      canvas.drawRRect(
+        rrect,
+        Paint()
+          ..color = Colors.blueGrey
+          ..style = PaintingStyle.stroke
+          ..strokeWidth = 1.0,
+      );
+
+      canvas.restore();
+    }
+  }
+}

+ 575 - 0
lib/play/board_play.dart

@@ -0,0 +1,575 @@
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:image_puzzle/config/device.dart';
+import 'package:image_puzzle/play/board.dart';
+import 'package:image_puzzle/play/board_painter.dart';
+import 'package:image_puzzle/play/piece.dart';
+import 'package:logging/logging.dart';
+import 'package:provider/provider.dart';
+import 'dart:ui' as ui;
+import 'package:vector_math/vector_math.dart' as vmath;
+
+final Logger _log = Logger('board_play.dart');
+
+// 移动类型 (不再需要,但保留枚举以防止其他文件引用报错)
+enum MoveType {
+  group, // 整个群组一起移动
+  single, // 单个碎片移动
+}
+
+// 操作类型
+enum Action {
+  revert, // 回归
+  swap, // 交换
+}
+
+class BoardPlay extends StatefulWidget {
+  final int cols;
+  final int rows;
+
+  const BoardPlay({super.key, required this.cols, required this.rows});
+
+  @override
+  State<StatefulWidget> createState() {
+    return _BoardPlayState();
+  }
+}
+
+class _BoardPlayState extends State<BoardPlay> with TickerProviderStateMixin {
+  final GlobalKey boardKey = GlobalKey();
+  Board? board;
+  bool _isLoading = true;
+
+  Piece? _draggingPiece;
+
+  // 记录所有动画中的移动项 (位移/交换/归位)
+  List<MoveItem>? moveItems;
+
+  // 动画控制器
+  late AnimationController _moveAnimationController; // 移动动画(位移)
+  late AnimationController _mergeAnimationController; // merge动画(scale)
+
+  // merge 动画的缩放值
+  late Animation<double> _mergeScaleAnimation;
+
+  List<PieceGroup>? _mergeGroups;
+
+  @override
+  initState() {
+    super.initState();
+    _moveAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
+    _moveAnimationController.addListener(_moveAnimationListener);
+    _moveAnimationController.addStatusListener(_moveAnimationStatusListener);
+
+    // 初始化 Merge 动画
+    _mergeAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); // 0.4s for scale up/down
+    _mergeAnimationController.addListener(_mergeAnimationListener);
+    _mergeAnimationController.addStatusListener(_mergeAnimationStatusListener);
+
+    // 缩放值从 1.0 -> 1.1 -> 1.0 (使用 TweenSequence 实现放大再缩小)
+    _mergeScaleAnimation =
+        TweenSequence<double>([
+          TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.06), weight: 50),
+          TweenSequenceItem(tween: Tween<double>(begin: 1.06, end: 1.0), weight: 50),
+        ]).animate(
+          // 应用曲线:用 CurvedAnimation 包装控制器
+          CurvedAnimation(
+            parent: _mergeAnimationController, // 动画控制器
+            curve: Curves.easeInOut, // 曲线类型(先加速后减速)
+          ),
+        );
+
+    init();
+  }
+
+  // 关键修正:动画监听器,只注册一次
+  void _moveAnimationListener() {
+    if (moveItems == null || moveItems!.isEmpty) {
+      return;
+    }
+
+    for (var item in moveItems!) {
+      item.move();
+    }
+
+    board!.invalidate();
+  }
+
+  void _moveAnimationStatusListener(AnimationStatus status) {
+    if (status == AnimationStatus.completed) {
+      // 动画完成,确保所有 piece 都在它们的最终位置(endTransform)
+      if (moveItems != null) {
+        bool needRebuildGroup = moveItems!.any((item) => item.action == Action.swap);
+
+        // 确保所有 piece 的 transform 最终停留在 endTransform
+        for (var item in moveItems!) {
+          item.stop();
+        }
+
+        if (needRebuildGroup) {
+          board!.backupAllGroups();
+          board!.rebuildAllGroups();
+          final mergeGroups = board!.compareAllGroups();
+
+          // 有新的区块合成,执行 merge 动画,稍稍放大然后再复原
+          if (mergeGroups.isNotEmpty) {
+            _log.info('Merge animation start for ${mergeGroups.length} groups.');
+
+            // 启动 Merge 动画
+            _mergeGroups = mergeGroups;
+            _mergeAnimationController.forward(from: 0.0);
+
+            // 如果执行了 merge 动画,将胜利条件检查推迟到 merge 动画完成时
+            moveItems = null;
+            return;
+          }
+        }
+
+        // 如果没有 merge 发生,或者 move action 是 revert,检查胜利条件
+        if (board!.checkWinCondition()) {
+          board!.success();
+        }
+
+        moveItems = null;
+      }
+    }
+  }
+
+  // 新增:Merge 动画监听器
+  void _mergeAnimationListener() {
+    if (_mergeGroups == null || _mergeGroups!.isEmpty || board == null) {
+      return;
+    }
+
+    // 当前缩放值 (从 1.0 -> 1.1 -> 1.0)
+    final double scale = _mergeScaleAnimation.value;
+
+    for (var group in _mergeGroups!) {
+      final groupCenter = group.center;
+      for (var piece in group.pieces) {
+        // 1. 获取碎片归位后的基础位置(纯平移)
+        final baseTransform = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
+
+        // 2. 计算碎片左上角到群组中心的偏移量
+        final pieceTopLeft = Offset(baseTransform.storage[12], baseTransform.storage[13]);
+        final offsetToCenter = groupCenter - pieceTopLeft;
+
+        // 3. 创建围绕群组中心的缩放矩阵
+        final scaleMatrix = vmath.Matrix4.identity()
+          ..translate(offsetToCenter.dx, offsetToCenter.dy) // 移到群组中心
+          ..scale(scale, scale, 1.0) // 缩放
+          ..translate(-offsetToCenter.dx, -offsetToCenter.dy); // 移回原位
+
+        // 4. 应用最终变换
+        piece.transform = baseTransform * scaleMatrix;
+      }
+    }
+
+    board!.invalidate();
+  }
+
+  // 新增:Merge 动画状态监听器
+  void _mergeAnimationStatusListener(AnimationStatus status) {
+    if (status == AnimationStatus.completed) {
+      if (_mergeGroups != null && _mergeGroups!.isNotEmpty) {
+        for (var group in _mergeGroups!) {
+          for (var piece in group.pieces) {
+            piece.transform = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
+          }
+        }
+      }
+
+      _mergeGroups = null;
+
+      // 检查胜利条件 (现在所有 pieces 都在正确位置且 scale=1.0)
+      if (board!.checkWinCondition()) {
+        board!.success();
+      }
+
+      board!.invalidate();
+    }
+  }
+
+  init() async {
+    Device device = context.read<Device>();
+
+    setState(() {
+      _isLoading = true;
+    });
+
+    final targetRect = device.targetRect;
+    final bestImageSize = device.bestImageSize;
+
+    // TODO: 替换为实际的图片加载逻辑
+    final ByteData data = await rootBundle.load('assets/images/test.jpeg');
+    final ui.Codec codec = await ui.instantiateImageCodec(
+      data.buffer.asUint8List(),
+      targetWidth: bestImageSize.width.round(),
+      targetHeight: bestImageSize.height.round(),
+    );
+    final ui.FrameInfo frameInfo = await codec.getNextFrame();
+    final image = frameInfo.image;
+
+    board = Board(this, image, widget.cols, widget.rows, targetRect, device);
+    board!.start(); // 游戏开始
+
+    setState(() {
+      _isLoading = false;
+    });
+  }
+
+  @override
+  void didChangeDependencies() async {
+    super.didChangeDependencies();
+    _log.info("didChangeDependencies");
+  }
+
+  @override
+  dispose() {
+    _moveAnimationController.removeListener(_moveAnimationListener);
+    _moveAnimationController.removeStatusListener(_moveAnimationStatusListener);
+    _moveAnimationController.dispose();
+
+    _mergeAnimationController.removeListener(_mergeAnimationListener);
+    _mergeAnimationController.removeStatusListener(_mergeAnimationStatusListener);
+    _mergeAnimationController.dispose();
+
+    board?.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Device device = context.read<Device>();
+
+    return Scaffold(
+      body: Stack(
+        children: <Widget>[
+          if (!_isLoading) Positioned.fill(child: _buildPuzzleCanvas(device.screenSize.width, device.screenSize.height)),
+          Positioned(top: 0, left: 0, right: 0, child: appBar),
+          Positioned(
+            bottom: 0,
+            left: 0,
+            right: 0,
+            child: Container(
+              height: device.bannerHeight,
+              color: Colors.green.shade100,
+              alignment: Alignment.center,
+              child: const Text('Banner 广告区域', style: TextStyle(fontSize: 12)),
+            ),
+          ),
+          if (_isLoading) const Positioned.fill(child: Center(child: CircularProgressIndicator())),
+        ],
+      ),
+    );
+  }
+
+  final appBar = SafeArea(
+    child: Center(
+      child: Padding(
+        padding: const EdgeInsets.all(10.0),
+        child: Text(
+          '关卡1',
+          style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600),
+        ),
+      ),
+    ),
+  );
+
+  Widget _buildPuzzleCanvas(double width, double height) {
+    return CustomPaint(
+      painter: BoardPainter(board: board!),
+      size: Size(width, height),
+      child: GestureDetector(
+        key: boardKey,
+        onPanStart: _onPanStart,
+        onPanUpdate: _onPanUpdate,
+        onPanEnd: _onPanEnd,
+        child: Container(color: Colors.transparent),
+      ),
+    );
+  }
+
+  Offset _globalToLocal(Offset globalPosition) {
+    final RenderBox renderBox = boardKey.currentContext!.findRenderObject() as RenderBox;
+    return renderBox.globalToLocal(globalPosition);
+  }
+
+  void _onPanStart(DragStartDetails details) {
+    _log.info('_onPanStart');
+    // 动画中断逻辑:如果动画正在进行,立即停止并强制所有 pieces 归位到最终位置
+    if (_moveAnimationController.isAnimating && moveItems != null) {
+      _log.info('移动动画中断,强制归位/交换');
+      for (var item in moveItems!) {
+        item.stop();
+      }
+
+      bool needRebuildGroup = moveItems!.any((item) => item.action == Action.swap);
+      if (needRebuildGroup) {
+        board!.backupAllGroups();
+        board!.rebuildAllGroups();
+        final mergeGroups = board!.compareAllGroups();
+
+        if (mergeGroups.isNotEmpty) {
+          // 此时不需要触发 merge 动画,只需确保数据结构正确
+        }
+      }
+
+      moveItems = null; // 清空动画列表
+      _moveAnimationController.stop();
+      board!.invalidate(); // 触发一次重绘来显示最终位置
+    }
+
+    // 如果 merge 动画正在进行,也应该中断并立即归位
+    if (_mergeAnimationController.isAnimating && _mergeGroups != null) {
+      _log.info('合并动画中断,强制归位');
+
+      for (var group in _mergeGroups!) {
+        for (var piece in group.pieces) {
+          piece.transform = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
+        }
+      }
+      _mergeGroups = null;
+      _mergeAnimationController.stop();
+      board!.invalidate();
+    }
+
+    // 停止所有正在运行的动画(如果尚未停止)
+    _moveAnimationController.stop();
+    _mergeAnimationController.stop();
+    moveItems = null;
+
+    final localPosition = _globalToLocal(details.globalPosition);
+    final touchedPiece = board?.findPieceAt(localPosition);
+
+    if (touchedPiece != null) {
+      _draggingPiece = touchedPiece;
+
+      final draggingGroup = _draggingPiece!.group;
+      if (draggingGroup != null) {
+        // 将拖拽群组置于 pieces 列表末尾,确保它在 CustomPainter 中被最后绘制(在最上层)
+        board!.pieces.removeWhere((p) => draggingGroup.contains(p));
+        board!.pieces.addAll(draggingGroup.pieces);
+      } else {
+        board!.pieces.remove(_draggingPiece);
+        board!.pieces.add(_draggingPiece!);
+      }
+      board!.invalidate();
+    }
+  }
+
+  void _onPanUpdate(DragUpdateDetails details) {
+    _log.info('_onPanUpdate');
+    if (_draggingPiece == null) return;
+
+    final Offset delta = details.delta;
+
+    final draggingGroup = _draggingPiece!.group;
+    if (draggingGroup != null) {
+      // 拖拽过程中,所有群组成员共享相同的位移
+      for (var piece in draggingGroup.pieces) {
+        piece.applyDelta(delta);
+      }
+    } else {
+      _draggingPiece!.applyDelta(delta);
+    }
+
+    board!.invalidate();
+  }
+
+  void _onPanEnd(DragEndDetails details) {
+    _log.info('_onPanEnd');
+    if (_draggingPiece == null) {
+      return;
+    }
+
+    // 保存当前拖拽结束的碎片,以备动画使用
+    Piece leaderPiece = _draggingPiece!;
+    _draggingPiece = null; // 结束拖拽
+
+    board!.invalidate();
+
+    /// 交换或归位
+
+    // 获取碎片的中心点,判断中心点是否落到某个piece上
+    Piece? targetPiece = board!.findPieceAtExclude(leaderPiece.currentCenter, leaderPiece);
+
+    // 群组特殊处理:如果 leaderPiece 没有落在其他碎片上,检查群组其他成员
+    if (targetPiece == null && leaderPiece.group != null) {
+      for (var p in leaderPiece.group!.pieces) {
+        targetPiece = board!.findPieceAtExclude(p.currentCenter, p);
+        if (targetPiece != null) {
+          _log.info('推举 ${p.toString()} 为新leader');
+          leaderPiece = p; // p 落在有效的其他piece上,推举为leaderPiece
+          break;
+        }
+      }
+    }
+
+    // 判断是否可以交换
+    if (targetPiece != null && targetPiece != leaderPiece && leaderPiece.canPlaceTo(targetPiece)) {
+      _log.info("swap animation start");
+      _animateSwap(leaderPiece, targetPiece);
+    } else {
+      _log.info("revert animation start");
+      _animateRevert(leaderPiece);
+    }
+  }
+
+  // 关键重构:为所有涉及移动的 piece 创建独立的 MoveItem
+  void _animateSwap(Piece leaderPiece, Piece targetPiece) {
+    List<MoveItem> items = [];
+
+    // 1. 确定涉及移动的所有碎片
+    final List<Piece> draggingPieces = leaderPiece.group != null ? leaderPiece.group!.pieces : [leaderPiece];
+    final int dr = targetPiece.curRow - leaderPiece.curRow; // 目标位置的行位移
+    final int dc = targetPiece.curCol - leaderPiece.curCol; // 目标位置的列位移
+
+    // 2. 识别被替换/推开的碎片
+    List<Piece> displacedPieces = [];
+    if (leaderPiece.group != null) {
+      // 群组交换:找到群组新目标位置上的所有非自身群组的碎片
+      for (var p in draggingPieces) {
+        final int targetRow = p.curRow + dr;
+        final int targetCol = p.curCol + dc;
+        final Piece? other = board!.getPieceByCoordinate(targetRow, targetCol);
+        if (other != null && !p.isSameGroup(other)) {
+          displacedPieces.add(other);
+        }
+      }
+    } else {
+      // 单碎片交换:被替换的碎片就是 targetPiece
+      displacedPieces.add(targetPiece);
+    }
+
+    // 3. 更新逻辑坐标 (curRow, curCol) - 必须在创建动画前完成
+
+    // a. 更新拖拽群组/碎片的位置
+    for (var p in draggingPieces) {
+      p.curRow += dr;
+      p.curCol += dc;
+    }
+
+    // b. 更新被推开的碎片的位置 (移入拖拽群组腾出的槽位)
+    if (leaderPiece.group == null) {
+      // 单碎片交换:targetPiece 移入 leaderPiece 的旧槽位
+      targetPiece.curRow -= dr;
+      targetPiece.curCol -= dc;
+    } else {
+      // 群组交换:被推开的碎片向后退 dr/dc 距离,找到空槽位
+      for (var p in displacedPieces) {
+        int newRow = p.curRow - dr;
+        int newCol = p.curCol - dc;
+
+        do {
+          final Piece? pieceInSlot = board!.getPieceByCoordinate(newRow, newCol);
+          if (pieceInSlot == null) {
+            p.curRow = newRow;
+            p.curCol = newCol;
+            break;
+          } else {
+            newRow -= dr;
+            newCol -= dc;
+          }
+        } while (newRow >= 0 && newRow < p.rows && newCol >= 0 && newCol < p.cols);
+      }
+    }
+
+    // 4. 为所有涉及移动的碎片创建 MoveItem
+
+    // a. 拖拽群组/碎片
+    for (var p in draggingPieces) {
+      // 动画起点:拖拽结束时的实际 Canvas 坐标 (p.transform)
+      final startTransform = p.transform;
+      // 动画终点:新的逻辑网格坐标
+      final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol);
+      final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController);
+
+      items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.swap));
+    }
+
+    // b. 被推开的碎片
+    for (var p in displacedPieces) {
+      // 动画起点:旧的逻辑网格坐标 (p.transform)
+      final startTransform = p.transform;
+      // 动画终点:新的逻辑网格坐标
+      final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol);
+      final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController);
+
+      items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.swap));
+    }
+
+    // 5. 启动动画
+    moveItems = items;
+    _moveAnimationController.forward(from: 0.0);
+  }
+
+  // 关键重构:为所有涉及归位的 piece 创建独立的 MoveItem
+  void _animateRevert(Piece piece) {
+    List<MoveItem> items = [];
+    final List<Piece> groupPieces = piece.group != null ? piece.group!.pieces : [piece];
+
+    for (var p in groupPieces) {
+      // 动画起点:拖拽结束时的实际 Canvas 坐标 (p.transform)
+      final startTransform = p.transform;
+      // 动画终点:归位位置(即拖拽前所在的逻辑网格坐标)
+      final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol);
+      final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController);
+
+      items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.revert));
+    }
+
+    moveItems = items;
+    _moveAnimationController.forward(from: 0.0);
+  }
+}
+
+// 辅助类:用于对 vmath.Matrix4 进行线性插值 (lerp),实现平滑动画
+class Matrix4Tween extends Tween<vmath.Matrix4> {
+  Matrix4Tween({required vmath.Matrix4 begin, required vmath.Matrix4 end}) : super(begin: begin, end: end);
+
+  @override
+  vmath.Matrix4 lerp(double t) {
+    if (begin == null || end == null) return begin ?? end ?? vmath.Matrix4.identity();
+
+    final List<double> lerpedStorage = List.generate(16, (i) {
+      // 确保使用 ui.lerpDouble 进行插值
+      return ui.lerpDouble(begin!.storage[i], end!.storage[i], t)!;
+    });
+
+    return vmath.Matrix4.fromList(lerpedStorage.cast<double>());
+  }
+}
+
+// 动画辅助类,记录移动信息
+class MoveItem {
+  // 要移动的piece
+  final Piece piece;
+  // 动画animation
+  final Animation<vmath.Matrix4> animation;
+  // 起始位置(拖拽结束时的实际 Canvas 坐标)
+  final vmath.Matrix4 startTransform;
+  // 结束位置(目标网格槽位的 Canvas 坐标)
+  final vmath.Matrix4 endTransform;
+
+  // 移除了 MoveType,因为现在每个 piece 都有自己的 MoveItem
+  // final MoveType moveType;
+
+  final Action action;
+
+  MoveItem({required this.piece, required this.animation, required this.startTransform, required this.endTransform, required this.action});
+
+  // 关键修正:直接设置 piece 的 transform 为动画插值
+  void move() {
+    // 关键修正:直接设置piece的transform为动画插值,而不是累加delta
+    piece.transform = animation.value;
+  }
+
+  void stop() {
+    // 关键修正:动画中断时,直接设置到最终目标位置 (endTransform)
+    // 此时 piece 的 curRow/curCol 已经是目标网格坐标
+    piece.transform = endTransform;
+  }
+}

+ 575 - 0
lib/play/piece.dart

@@ -0,0 +1,575 @@
+// piece.dart
+
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:image_puzzle/play/board.dart';
+import 'package:logging/logging.dart';
+import 'package:vector_math/vector_math.dart' as vmath;
+
+final Logger _log = Logger('piece.dart');
+
+// piece分组数据结构
+class PieceGroup {
+  List<Piece> pieces = [];
+
+  int get length => pieces.length;
+
+  void add(Piece piece) {
+    if (!pieces.contains(piece)) {
+      pieces.add(piece);
+    }
+    piece.group = this;
+  }
+
+  void remove(Piece piece) {
+    if (pieces.contains(piece)) {
+      pieces.remove(piece);
+    }
+    piece.group = null;
+  }
+
+  bool contains(Piece piece) {
+    return pieces.contains(piece);
+  }
+
+  // 判断本group是否包含other group
+  bool containsGroup(PieceGroup otherGroup) {
+    for (var p in otherGroup.pieces) {
+      if (!pieces.contains(p)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // 群组中心点
+  Offset get center {
+    if (pieces.isEmpty) return Offset.zero;
+
+    final board = pieces[0].board;
+
+    // 计算群组在Canvas中的边界框
+    double minX = double.infinity;
+    double minY = double.infinity;
+    double maxX = -double.infinity;
+    double maxY = -double.infinity;
+
+    for (var piece in pieces) {
+      final transform = piece.transform;
+      final x = transform.storage[12]; // 碎片左上角x
+      final y = transform.storage[13]; // 碎片左上角y
+      final w = board.pieceLogicalWidth;
+      final h = board.pieceLogicalHeight;
+
+      // 更新边界
+      minX = min(minX, x);
+      minY = min(minY, y);
+      maxX = max(maxX, x + w);
+      maxY = max(maxY, y + h);
+    }
+
+    // 计算边界框中心点
+    return Offset((minX + maxX) / 2, (minY + maxY) / 2);
+  }
+
+  void print() {
+    String str = '======= group size: $length =======\n';
+    for (var p in pieces) {
+      str += p.toString();
+      str += '\n';
+    }
+
+    _log.info(str);
+  }
+}
+
+// 碎片基础数据结构
+class Piece {
+  final int index;
+
+  final Board board; // 保存board的引用
+
+  PieceGroup? group;
+
+  // 总计的行数和列数
+  final int rows;
+  final int cols;
+
+  // 碎片在完整图片中的行/列位置 (逻辑坐标)
+  final int row;
+  final int col;
+
+  // 碎片当前所处的位置
+  int curCol;
+  int curRow;
+
+  // 正确目标位置的左上角坐标 (在 Canvas 坐标系内,这是碎片的最终位置)
+  final Offset correctOffset;
+
+  // 碎片的当前几何变换状态 (包括位置、旋转等)
+  vmath.Matrix4 transform;
+
+  // 碎片在原图片中的裁剪矩形 (Source Rect of the image)
+  final Rect sourceRect;
+
+  // 5. 碎片周围四个边的拼接状态 (用于动态绘制内部边框)
+  // [Top, Right, Bottom, Left]
+  List<bool> borders; // 拟废弃,采用实时计算
+
+  bool get isOK => row == curRow && col == curCol;
+
+  // clipPath, 碎片的裁剪路径(闭合), 这是为了绘制出圆角效果
+  Path? path;
+  // 内边框path(可能非闭合)
+  Path? innerLinePath;
+  // 外边框path (可能非闭合)
+  Path? outLinePath;
+
+  static const double _cornerRadius = 8.0;
+  static const double _outLineOffset = 0.5;
+  static const double _innerLineOffset = 1.5;
+
+  Piece({
+    required this.board,
+    required this.index,
+    required this.row,
+    required this.col,
+    required this.rows,
+    required this.cols,
+    required this.correctOffset,
+    required this.sourceRect,
+    required this.curCol,
+    required this.curRow,
+    required this.transform,
+    required this.borders, // 初始设置为 [true, true, true, true]
+  });
+
+  @override
+  String toString() {
+    return 'Piece($index,$row:$col)';
+  }
+
+  double get width => board.pieceLogicalWidth;
+  double get height => board.pieceLogicalHeight;
+
+  // 辅助函数:获取碎片当前的左上角位置 (tx, ty)
+  // 平移分量在 Matrix4.storage 的索引 12 (tx) 和 13 (ty)
+  Offset get currentOffset => Offset(transform.storage[12], transform.storage[13]);
+
+  // 辅助函数:获取碎片当前的中心点 (在 Canvas 坐标系中)
+  Offset get currentCenter {
+    // 碎片的本地中心点
+    final Offset pieceLocalCenter = Offset(board.pieceLogicalWidth / 2, board.pieceLogicalHeight / 2);
+
+    // 应用当前变换矩阵
+    final vmath.Vector4 transformedVector = transform.transform(vmath.Vector4(pieceLocalCenter.dx, pieceLocalCenter.dy, 0.0, 1.0));
+
+    return Offset(transformedVector.x, transformedVector.y);
+  }
+
+  // 辅助函数:将碎片移动指定的位移量
+  void applyDelta(Offset delta) {
+    // 累加 x 轴位移
+    transform.storage[12] += delta.dx;
+    // 累加 y 轴位移
+    transform.storage[13] += delta.dy;
+  }
+
+  // 归位, 回到原来的位置
+  void revert() {
+    final originalTransform = board.getTransformByCoordinate(curRow, curCol);
+    transform = originalTransform;
+  }
+
+  // 判断当前piece是否可以安置到other的槽位去
+  bool canPlaceTo(Piece other) {
+    // 1. 如果当前碎片是单独移动,则可以安置
+    if (group == null) {
+      return true;
+    }
+
+    // 当前 piece 与 other piece 的位移
+    int dr = other.curRow - curRow;
+    int dc = other.curCol - curCol;
+
+    // 判断当前piece的组成员,产生相同的位移后会不会溢出
+    for (var p in group!.pieces) {
+      int newRow = p.curRow + dr;
+      int newCol = p.curCol + dc;
+      if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  // 判断当前piece是否可以和other piece 合并
+  bool canMerge(Piece other) {
+    // 原来是邻居, 现在也是邻居,才具备可以合并的基础条件
+    if (isNeighbour(other) && isCurNeighbour(other)) {
+      // 判断相对位置是否保持一致 (防止错位拼接)
+      if (col == other.col) {
+        // 同一列 (上/下相邻)
+        if ((row - other.row) == (curRow - other.curRow)) return true;
+      } else if (row == other.row) {
+        // 同一行 (左/右相邻)
+        if ((col - other.col) == (curCol - other.curCol)) return true;
+      }
+    }
+    return false;
+  }
+
+  /// 创建一个新组, 合并两个piece组
+  void groupWith(Piece other) {
+    PieceGroup finalGroup = PieceGroup();
+    List<Piece> list = [];
+    if (group != null) list.addAll(group!.pieces);
+    if (other.group != null) list.addAll(other.group!.pieces);
+    list.add(this);
+    list.add(other);
+    for (var p in list) {
+      finalGroup.add(p);
+    }
+  }
+
+  // 获取碎片本来的邻居(原正确位置上的邻居)
+  List<int> getNeighbourIndexes() {
+    List<int> list = [];
+    if (row > 0) list.add(index - cols); // 上
+    if (row < (rows - 1)) list.add(index + cols); // 下
+    if (col > 0) list.add(index - 1); // 左
+    if (col < (cols - 1)) list.add(index + 1); // 右
+    return list;
+  }
+
+  /// 是否同一个组
+  bool isSameGroup(Piece other) {
+    return group != null && group == other.group;
+  }
+
+  /// 是否邻居 (原始位置)
+  bool isNeighbour(Piece other) {
+    return row == other.row && (col - other.col).abs() == 1 || col == other.col && (row - other.row).abs() == 1;
+  }
+
+  // 当前位置是否是邻居
+  bool isCurNeighbour(Piece other) {
+    return curRow == other.curRow && (curCol - other.curCol).abs() == 1 || curCol == other.curCol && (curRow - other.curRow).abs() == 1;
+  }
+
+  // 是否有上边框
+  bool get _hasTopBorder {
+    int topCurRow = curRow - 1;
+    int topCurCol = curCol;
+
+    // 已经是顶部的宫格, 需要上边框
+    if (topCurRow < 0) return true;
+
+    // 获取本碎片当前的上边邻居
+    final topPiece = board.getPieceByCoordinate(topCurRow, topCurCol);
+    if (topPiece == null) {
+      _log.warning('找不到 ${toString()} 的上邻居,有错误发生,请检查');
+      return true;
+    }
+    // 如果与上邻居的相对位置是正确的,那么没有上边框
+    if (row == topPiece.row + 1 && col == topPiece.col) {
+      return false;
+    }
+
+    return true;
+  }
+
+  // 是否有右边框
+  bool get _hasRightBorder {
+    int rightCurRow = curRow;
+    int rightCurCol = curCol + 1;
+
+    // 已经是最右边的宫格, 需要右边框
+    if (rightCurCol >= cols) return true;
+
+    // 获取本碎片当前的右边邻居
+    final rightPiece = board.getPieceByCoordinate(rightCurRow, rightCurCol);
+    if (rightPiece == null) {
+      _log.warning('找不到 ${toString()} 的右邻居,有错误发生,请检查');
+      return true;
+    }
+    // 如果与右邻居的相对位置是正确的,那么没有右边框
+    if (row == rightPiece.row && col == rightPiece.col - 1) {
+      return false;
+    }
+
+    return true;
+  }
+
+  // 是否有下边框
+  bool get _hasBottomBorder {
+    int bottomCurRow = curRow + 1;
+    int bottomCurCol = curCol;
+
+    // 已经是底部的宫格, 需要下边框
+    if (bottomCurRow >= rows) return true;
+
+    // 获取本碎片当前的底边邻居
+    final bottomPiece = board.getPieceByCoordinate(bottomCurRow, bottomCurCol);
+    if (bottomPiece == null) {
+      _log.warning('找不到 ${toString()} 的下邻居,有错误发生,请检查');
+      return true;
+    }
+    // 如果与下邻居的相对位置是正确的,那么没有下边框
+    if (row == bottomPiece.row - 1 && col == bottomPiece.col) {
+      return false;
+    }
+
+    return true;
+  }
+
+  // 是否有左边框
+  bool get _hasLeftBorder {
+    int leftCurRow = curRow;
+    int leftCurCol = curCol - 1;
+
+    // 已经是最左部的宫格, 需要左边框
+    if (leftCurCol < 0) return true;
+
+    // 获取本碎片当前的左边邻居
+    final leftPiece = board.getPieceByCoordinate(leftCurRow, leftCurCol);
+    if (leftPiece == null) {
+      _log.warning('找不到 ${toString()} 的左邻居,有错误发生,请检查');
+      return true;
+    }
+    // 如果与下邻居的相对位置是正确的,那么没有下边框
+    if (row == leftPiece.row && col == leftPiece.col + 1) {
+      return false;
+    }
+
+    return true;
+  }
+
+  // 生成clip path
+  Path _generateClipPath(double w, double h, List<bool> borders, double radius) {
+    Path path = Path();
+
+    // 一个角是否为圆角取决于相邻的两条边是否都需要绘制 (即 borders 都为 true)
+    final bool tlRounded = borders[0] && borders[3]; // Top && Left
+    final bool trRounded = borders[0] && borders[1]; // Top && Right
+    final bool brRounded = borders[2] && borders[1]; // Bottom && Right
+    final bool blRounded = borders[2] && borders[3]; // Bottom && Left
+
+    // 1. 移动到起点 (左上角,T 边直线段的起点)
+    if (tlRounded) {
+      path.moveTo(radius, 0);
+    } else {
+      path.moveTo(0, 0); // 尖角起点
+    }
+    // A. 顶边 (T) - LineTo
+    // 终点是 TR 圆角的起点 (w - radius, 0) 或 TR 尖角 (w, 0)
+    if (trRounded) {
+      path.lineTo(w - radius, 0);
+    } else {
+      path.lineTo(w, 0);
+    }
+
+    // B. 右上角 (TR) - Arc
+    if (trRounded) {
+      path.arcToPoint(
+        Offset(w, radius), // 终点 (w, radius)
+        radius: Radius.circular(radius),
+        clockwise: true, // 逆时针
+      );
+    }
+
+    // C. 右边 (R) - LineTo
+    // 终点是 BR 圆角的起点 (w, h - radius) 或 BR 尖角 (w, h)
+    if (brRounded) {
+      path.lineTo(w, h - radius);
+    } else {
+      path.lineTo(w, h);
+    }
+
+    // D. 右下角 (BR) - Arc
+    if (brRounded) {
+      path.arcToPoint(
+        Offset(w - radius, h), // 终点 (w - radius, h)
+        radius: Radius.circular(radius),
+        clockwise: true, // 逆时针
+      );
+    }
+
+    // E. 底边 (B) - LineTo
+    // 终点是 BL 圆角的起点 (radius, h) 或 BL 尖角 (0, h)
+    if (blRounded) {
+      path.lineTo(radius, h);
+    } else {
+      path.lineTo(0, h);
+    }
+
+    // F. 左下角 (BL) - Arc
+    if (blRounded) {
+      path.arcToPoint(
+        Offset(0, h - radius), // 终点 (0, h - radius)
+        radius: Radius.circular(radius),
+        clockwise: true, // 逆时针
+      );
+    }
+
+    // G. 左边 (L) - LineTo
+    // 终点是 TL 圆角的起点 (0, radius) 或 TL 尖角 (0, 0)
+    if (tlRounded) {
+      path.lineTo(0, radius);
+    } else {
+      path.lineTo(0, 0);
+    }
+
+    // H. 左上角 (TL) - Arc & Close
+    if (tlRounded) {
+      path.arcToPoint(
+        Offset(radius, 0), // 终点 (radius, 0),即起点
+        radius: Radius.circular(radius),
+        clockwise: true, // 逆时针
+      );
+    }
+    // path.close() 确保路径闭合,但上面的逻辑已经把路径连回了起点。
+    path.close();
+
+    return path;
+  }
+
+  // 生成border path (重构为开放路径,仅绘制需要的边)
+  Path _generateBorderPath(double w, double h, List<bool> borders, double radius, double offset) {
+    Path path = Path();
+    final double r = radius;
+
+    // 边界调整后的坐标
+    final double x0 = offset; // 左边界
+    final double y0 = offset; // 上边界
+    final double x1 = w - offset; // 右边界
+    final double y1 = h - offset; // 下边界
+
+    // 关键点坐标 (圆角弧的起点/终点)
+    final Offset pTL_T = Offset(x0 + r, y0); // Top边起点
+    final Offset pTR_T = Offset(x1 - r, y0); // Top边终点
+    final Offset pTR_R = Offset(x1, y0 + r); // Right边起点
+    final Offset pBR_R = Offset(x1, y1 - r); // Right边终点
+    final Offset pBR_B = Offset(x1 - r, y1); // Bottom边起点
+    final Offset pBL_B = Offset(x0 + r, y1); // Bottom边终点
+    final Offset pBL_L = Offset(x0, y1 - r); // Left边起点
+    final Offset pTL_L = Offset(x0, y0 + r); // Left边终点
+
+    // [Top, Right, Bottom, Left]
+    final bool hasT = borders[0];
+    final bool hasR = borders[1];
+    final bool hasB = borders[2];
+    final bool hasL = borders[3];
+
+    // --- 1. Top Border (T) ---
+    if (hasT) {
+      // 检查TL角是否是尖角 (Left边缺失)
+      if (hasL) {
+        path.moveTo(pTL_T.dx, pTL_T.dy); // 从TL弧的终点开始
+      } else {
+        path.moveTo(x0, y0); // 从左上尖角开始
+      }
+
+      // 绘制 Top 直线段
+      if (hasR) {
+        path.lineTo(pTR_T.dx, pTR_T.dy);
+      } else {
+        path.lineTo(x1, y0); // 到右上尖角
+      }
+    }
+
+    // --- 2. Top-Right Corner (TR) & Right Border (R) ---
+    if (hasT && hasR) {
+      // 绘制 TR 弧
+      path.arcToPoint(pTR_R, radius: Radius.circular(r), clockwise: true);
+    } else if (hasR) {
+      // T 缺失,R 存在:需要开始一个新的轮廓,从 TR 弧的起点开始 (pTR_R)
+      if (hasT) {
+        path.moveTo(pTR_R.dx, pTR_R.dy); // 从 TR 弧终点开始
+      } else {
+        path.moveTo(x1, y0); // 从右上尖角开始
+      }
+    }
+
+    if (hasR) {
+      // 绘制 Right 直线段
+      if (hasB) {
+        path.lineTo(pBR_R.dx, pBR_R.dy);
+      } else {
+        path.lineTo(x1, y1); // 到右下尖角
+      }
+    }
+
+    // --- 3. Bottom-Right Corner (BR) & Bottom Border (B) ---
+    if (hasR && hasB) {
+      // 绘制 BR 弧
+      path.arcToPoint(pBR_B, radius: Radius.circular(r), clockwise: true);
+    } else if (hasB) {
+      // R 缺失,B 存在:需要开始一个新的轮廓,从 BR 弧的起点开始 (pBR_B)
+      if (hasR) {
+        path.moveTo(pBR_B.dx, pBR_B.dy); // 从 BR 弧终点开始
+      } else {
+        path.moveTo(x1, y1); // 从右下尖角开始
+      }
+    }
+
+    if (hasB) {
+      // 绘制 Bottom 直线段
+      if (hasL) {
+        path.lineTo(pBL_B.dx, pBL_B.dy);
+      } else {
+        path.lineTo(x0, y1); // 到左下尖角
+      }
+    }
+
+    // --- 4. Bottom-Left Corner (BL) & Left Border (L) ---
+    if (hasB && hasL) {
+      // 绘制 BL 弧
+      path.arcToPoint(pBL_L, radius: Radius.circular(r), clockwise: true);
+    } else if (hasL) {
+      // B 缺失,L 存在:需要开始一个新的轮廓,从 BL 弧的起点开始 (pBL_L)
+      if (hasB) {
+        path.moveTo(pBL_L.dx, pBL_L.dy); // 从 BL 弧终点开始
+      } else {
+        path.moveTo(x0, y1); // 从左下尖角开始
+      }
+    }
+
+    if (hasL) {
+      // 绘制 Left 直线段
+      if (hasT) {
+        path.lineTo(pTL_L.dx, pTL_L.dy);
+      } else {
+        path.lineTo(x0, y0); // 到左上尖角
+      }
+    }
+
+    // --- 5. Top-Left Corner (TL) ---
+    if (hasL && hasT) {
+      // 绘制 TL 弧,连接回 Top Border 的起点
+      path.arcToPoint(pTL_T, radius: Radius.circular(r), clockwise: true);
+    }
+    // 注意: 不调用 path.close(),保持路径开放。
+
+    return path;
+  }
+
+  // 生成碎片的clipPath, innerLinePath, outLinePath
+  List<Path> generatePaths() {
+    // _log.info('${toString()} generatePaths');
+    // 先确定4条边的状态[Top, Right, Bottom, Left]
+    List<bool> borders = [true, true, true, true];
+    // 如果是单个碎片,4条边都需要,只有piece在group中才需要判断
+    if (group != null) {
+      borders = [_hasTopBorder, _hasRightBorder, _hasBottomBorder, _hasLeftBorder];
+    }
+
+    path = _generateClipPath(width, height, borders, _cornerRadius);
+    outLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _outLineOffset);
+    innerLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _innerLineOffset);
+
+    return [path!, outLinePath!, innerLinePath!];
+  }
+}

+ 1 - 0
linux/.gitignore

@@ -0,0 +1 @@
+flutter/ephemeral

+ 128 - 0
linux/CMakeLists.txt

@@ -0,0 +1,128 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.13)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "image_puzzle")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.example.image_puzzle")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+  set(CMAKE_BUILD_TYPE "Debug" CACHE
+    STRING "Flutter build mode" FORCE)
+  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+    "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+  target_compile_features(${TARGET} PUBLIC cxx_std_14)
+  target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+  target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
+  target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+  PROPERTIES
+  RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+  set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+  file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+  " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+  COMPONENT Runtime)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+  install(FILES "${bundled_library}"
+    DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endforeach(bundled_library)
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+   DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+   COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+  file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+  " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+  DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+  install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endif()

+ 88 - 0
linux/flutter/CMakeLists.txt

@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+    set(NEW_LIST "")
+    foreach(element ${${LIST_NAME}})
+        list(APPEND NEW_LIST "${PREFIX}${element}")
+    endforeach(element)
+    set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+  "fl_basic_message_channel.h"
+  "fl_binary_codec.h"
+  "fl_binary_messenger.h"
+  "fl_dart_project.h"
+  "fl_engine.h"
+  "fl_json_message_codec.h"
+  "fl_json_method_codec.h"
+  "fl_message_codec.h"
+  "fl_method_call.h"
+  "fl_method_channel.h"
+  "fl_method_codec.h"
+  "fl_method_response.h"
+  "fl_plugin_registrar.h"
+  "fl_plugin_registry.h"
+  "fl_standard_message_codec.h"
+  "fl_standard_method_codec.h"
+  "fl_string_codec.h"
+  "fl_value.h"
+  "fl_view.h"
+  "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+  "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+  PkgConfig::GTK
+  PkgConfig::GLIB
+  PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+    ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+  COMMAND ${CMAKE_COMMAND} -E env
+    ${FLUTTER_TOOL_ENVIRONMENT}
+    "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+  VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+  "${FLUTTER_LIBRARY}"
+  ${FLUTTER_LIBRARY_HEADERS}
+)

+ 15 - 0
linux/flutter/generated_plugin_registrant.cc

@@ -0,0 +1,15 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include <url_launcher_linux/url_launcher_plugin.h>
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+  g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+  url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+}

+ 15 - 0
linux/flutter/generated_plugin_registrant.h

@@ -0,0 +1,15 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include <flutter_linux/flutter_linux.h>
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif  // GENERATED_PLUGIN_REGISTRANT_

+ 24 - 0
linux/flutter/generated_plugins.cmake

@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+  url_launcher_linux
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)

+ 26 - 0
linux/runner/CMakeLists.txt

@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.13)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+  "main.cc"
+  "my_application.cc"
+  "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the application ID.
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

+ 6 - 0
linux/runner/main.cc

@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+  g_autoptr(MyApplication) app = my_application_new();
+  return g_application_run(G_APPLICATION(app), argc, argv);
+}

+ 130 - 0
linux/runner/my_application.cc

@@ -0,0 +1,130 @@
+#include "my_application.h"
+
+#include <flutter_linux/flutter_linux.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+  GtkApplication parent_instance;
+  char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+  MyApplication* self = MY_APPLICATION(application);
+  GtkWindow* window =
+      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+  // Use a header bar when running in GNOME as this is the common style used
+  // by applications and is the setup most users will be using (e.g. Ubuntu
+  // desktop).
+  // If running on X and not using GNOME then just use a traditional title bar
+  // in case the window manager does more exotic layout, e.g. tiling.
+  // If running on Wayland assume the header bar will work (may need changing
+  // if future cases occur).
+  gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+  GdkScreen* screen = gtk_window_get_screen(window);
+  if (GDK_IS_X11_SCREEN(screen)) {
+    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+    if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+      use_header_bar = FALSE;
+    }
+  }
+#endif
+  if (use_header_bar) {
+    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+    gtk_widget_show(GTK_WIDGET(header_bar));
+    gtk_header_bar_set_title(header_bar, "image_puzzle");
+    gtk_header_bar_set_show_close_button(header_bar, TRUE);
+    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+  } else {
+    gtk_window_set_title(window, "image_puzzle");
+  }
+
+  gtk_window_set_default_size(window, 1280, 720);
+  gtk_widget_show(GTK_WIDGET(window));
+
+  g_autoptr(FlDartProject) project = fl_dart_project_new();
+  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
+
+  FlView* view = fl_view_new(project);
+  gtk_widget_show(GTK_WIDGET(view));
+  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+  fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+  gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
+  MyApplication* self = MY_APPLICATION(application);
+  // Strip out the first argument as it is the binary name.
+  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+  g_autoptr(GError) error = nullptr;
+  if (!g_application_register(application, nullptr, &error)) {
+     g_warning("Failed to register: %s", error->message);
+     *exit_status = 1;
+     return TRUE;
+  }
+
+  g_application_activate(application);
+  *exit_status = 0;
+
+  return TRUE;
+}
+
+// Implements GApplication::startup.
+static void my_application_startup(GApplication* application) {
+  //MyApplication* self = MY_APPLICATION(object);
+
+  // Perform any actions required at application startup.
+
+  G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
+}
+
+// Implements GApplication::shutdown.
+static void my_application_shutdown(GApplication* application) {
+  //MyApplication* self = MY_APPLICATION(object);
+
+  // Perform any actions required at application shutdown.
+
+  G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+  MyApplication* self = MY_APPLICATION(object);
+  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+  G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
+  G_APPLICATION_CLASS(klass)->startup = my_application_startup;
+  G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
+  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+  // Set the program name to the application ID, which helps various systems
+  // like GTK and desktop environments map this running application to its
+  // corresponding .desktop file. This ensures better integration by allowing
+  // the application to be recognized beyond its binary name.
+  g_set_prgname(APPLICATION_ID);
+
+  return MY_APPLICATION(g_object_new(my_application_get_type(),
+                                     "application-id", APPLICATION_ID,
+                                     "flags", G_APPLICATION_NON_UNIQUE,
+                                     nullptr));
+}

+ 18 - 0
linux/runner/my_application.h

@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include <gtk/gtk.h>
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+                     GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif  // FLUTTER_MY_APPLICATION_H_

+ 7 - 0
macos/.gitignore

@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/

+ 2 - 0
macos/Flutter/Flutter-Debug.xcconfig

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

+ 2 - 0
macos/Flutter/Flutter-Release.xcconfig

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

+ 16 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -0,0 +1,16 @@
+//
+//  Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import device_info_plus
+import path_provider_foundation
+import share_plus
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
+  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+  SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
+}

+ 42 - 0
macos/Podfile

@@ -0,0 +1,42 @@
+platform :osx, '10.14'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def flutter_root
+  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+  unless File.exist?(generated_xcode_build_settings_path)
+    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+  end
+
+  File.foreach(generated_xcode_build_settings_path) do |line|
+    matches = line.match(/FLUTTER_ROOT\=(.*)/)
+    return matches[1].strip if matches
+  end
+  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+  use_frameworks!
+
+  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+  target 'RunnerTests' do
+    inherit! :search_paths
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_macos_build_settings(target)
+  end
+end

+ 35 - 0
macos/Podfile.lock

@@ -0,0 +1,35 @@
+PODS:
+  - device_info_plus (0.0.1):
+    - FlutterMacOS
+  - FlutterMacOS (1.0.0)
+  - path_provider_foundation (0.0.1):
+    - Flutter
+    - FlutterMacOS
+  - share_plus (0.0.1):
+    - FlutterMacOS
+
+DEPENDENCIES:
+  - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
+  - FlutterMacOS (from `Flutter/ephemeral`)
+  - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
+  - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
+
+EXTERNAL SOURCES:
+  device_info_plus:
+    :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
+  FlutterMacOS:
+    :path: Flutter/ephemeral
+  path_provider_foundation:
+    :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
+  share_plus:
+    :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
+
+SPEC CHECKSUMS:
+  device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
+  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
+  path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
+  share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
+
+PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82
+
+COCOAPODS: 1.16.2

+ 801 - 0
macos/Runner.xcodeproj/project.pbxproj

@@ -0,0 +1,801 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 54;
+	objects = {
+
+/* Begin PBXAggregateTarget section */
+		33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+			isa = PBXAggregateTarget;
+			buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+			buildPhases = (
+				33CC111E2044C6BF0003C045 /* ShellScript */,
+			);
+			dependencies = (
+			);
+			name = "Flutter Assemble";
+			productName = FLX;
+		};
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+		304D1DC2547C6504F52F5A2C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75D9492CDC60FA6EFF2F21AA /* Pods_Runner.framework */; };
+		331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
+		335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+		33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+		33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+		33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+		E9F8A6026FF1874509350C6F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C985839716249050250C45D /* Pods_RunnerTests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+			remoteInfo = Runner;
+		};
+		33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+			remoteInfo = FLX;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		33CC110E2044A8840003C045 /* Bundle Framework */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Bundle Framework";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		0CB26364B0FC40330241B75A /* 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>"; };
+		2C985839716249050250C45D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
+		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
+		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
+		33CC10ED2044A3C60003C045 /* image_puzzle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = image_puzzle.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
+		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
+		33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
+		33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
+		33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
+		33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
+		33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
+		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
+		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
+		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+		3C88AB3FDA4AED7DFED0264A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
+		6387C011E3E4A1492C644D22 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
+		64F71DF4CBBB3DA6EA5564CE /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
+		75D9492CDC60FA6EFF2F21AA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+		B3B30D433313D186EE9C23C1 /* 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>"; };
+		D1DECC5EF1871951A7E8D13B /* 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>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		331C80D2294CF70F00263BE5 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				E9F8A6026FF1874509350C6F /* Pods_RunnerTests.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		33CC10EA2044A3C60003C045 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				304D1DC2547C6504F52F5A2C /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		331C80D6294CF71000263BE5 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
+		33BA886A226E78AF003329D5 /* Configs */ = {
+			isa = PBXGroup;
+			children = (
+				33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+			);
+			path = Configs;
+			sourceTree = "<group>";
+		};
+		33CC10E42044A3C60003C045 = {
+			isa = PBXGroup;
+			children = (
+				33FAB671232836740065AC1E /* Runner */,
+				33CEB47122A05771004F2AC0 /* Flutter */,
+				331C80D6294CF71000263BE5 /* RunnerTests */,
+				33CC10EE2044A3C60003C045 /* Products */,
+				D73912EC22F37F3D000D13A0 /* Frameworks */,
+				D84C09E609F6FFD2B1298007 /* Pods */,
+			);
+			sourceTree = "<group>";
+		};
+		33CC10EE2044A3C60003C045 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10ED2044A3C60003C045 /* image_puzzle.app */,
+				331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		33CC11242044D66E0003C045 /* Resources */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10F22044A3C60003C045 /* Assets.xcassets */,
+				33CC10F42044A3C60003C045 /* MainMenu.xib */,
+				33CC10F72044A3C60003C045 /* Info.plist */,
+			);
+			name = Resources;
+			path = ..;
+			sourceTree = "<group>";
+		};
+		33CEB47122A05771004F2AC0 /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+				33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+				33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+				33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+			);
+			path = Flutter;
+			sourceTree = "<group>";
+		};
+		33FAB671232836740065AC1E /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+				33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+				33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+				33E51914231749380026EE4D /* Release.entitlements */,
+				33CC11242044D66E0003C045 /* Resources */,
+				33BA886A226E78AF003329D5 /* Configs */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				75D9492CDC60FA6EFF2F21AA /* Pods_Runner.framework */,
+				2C985839716249050250C45D /* Pods_RunnerTests.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		D84C09E609F6FFD2B1298007 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				0CB26364B0FC40330241B75A /* Pods-Runner.debug.xcconfig */,
+				B3B30D433313D186EE9C23C1 /* Pods-Runner.release.xcconfig */,
+				D1DECC5EF1871951A7E8D13B /* Pods-Runner.profile.xcconfig */,
+				3C88AB3FDA4AED7DFED0264A /* Pods-RunnerTests.debug.xcconfig */,
+				6387C011E3E4A1492C644D22 /* Pods-RunnerTests.release.xcconfig */,
+				64F71DF4CBBB3DA6EA5564CE /* Pods-RunnerTests.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+			buildPhases = (
+				3A749A8D7453F02ADF8D173F /* [CP] Check Pods Manifest.lock */,
+				331C80D1294CF70F00263BE5 /* Sources */,
+				331C80D2294CF70F00263BE5 /* Frameworks */,
+				331C80D3294CF70F00263BE5 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+			);
+			name = RunnerTests;
+			productName = RunnerTests;
+			productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		33CC10EC2044A3C60003C045 /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				D92884008F4E119D2D6700AD /* [CP] Check Pods Manifest.lock */,
+				33CC10E92044A3C60003C045 /* Sources */,
+				33CC10EA2044A3C60003C045 /* Frameworks */,
+				33CC10EB2044A3C60003C045 /* Resources */,
+				33CC110E2044A8840003C045 /* Bundle Framework */,
+				3399D490228B24CF009A79C7 /* ShellScript */,
+				4C0A84824A94EFFA375959EE /* [CP] Embed Pods Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				33CC11202044C79F0003C045 /* PBXTargetDependency */,
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 33CC10ED2044A3C60003C045 /* image_puzzle.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		33CC10E52044A3C60003C045 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = YES;
+				LastSwiftUpdateCheck = 0920;
+				LastUpgradeCheck = 1510;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					331C80D4294CF70F00263BE5 = {
+						CreatedOnToolsVersion = 14.0;
+						TestTargetID = 33CC10EC2044A3C60003C045;
+					};
+					33CC10EC2044A3C60003C045 = {
+						CreatedOnToolsVersion = 9.2;
+						LastSwiftMigration = 1100;
+						ProvisioningStyle = Automatic;
+						SystemCapabilities = {
+							com.apple.Sandbox = {
+								enabled = 1;
+							};
+						};
+					};
+					33CC111A2044C6BA0003C045 = {
+						CreatedOnToolsVersion = 9.2;
+						ProvisioningStyle = Manual;
+					};
+				};
+			};
+			buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 33CC10E42044A3C60003C045;
+			productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				33CC10EC2044A3C60003C045 /* Runner */,
+				331C80D4294CF70F00263BE5 /* RunnerTests */,
+				33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		331C80D3294CF70F00263BE5 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		33CC10EB2044A3C60003C045 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+				33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3399D490228B24CF009A79C7 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+			);
+			outputFileListPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+		};
+		33CC111E2044C6BF0003C045 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				Flutter/ephemeral/FlutterInputs.xcfilelist,
+			);
+			inputPaths = (
+				Flutter/ephemeral/tripwire,
+			);
+			outputFileListPaths = (
+				Flutter/ephemeral/FlutterOutputs.xcfilelist,
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+		};
+		3A749A8D7453F02ADF8D173F /* [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-RunnerTests-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;
+		};
+		4C0A84824A94EFFA375959EE /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		D92884008F4E119D2D6700AD /* [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 */
+		331C80D1294CF70F00263BE5 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		33CC10E92044A3C60003C045 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+				33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+				335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 33CC10EC2044A3C60003C045 /* Runner */;
+			targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+		};
+		33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+			targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+			isa = PBXVariantGroup;
+			children = (
+				33CC10F52044A3C60003C045 /* Base */,
+			);
+			name = MainMenu.xib;
+			path = Runner;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		331C80DB294CF71000263BE5 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 3C88AB3FDA4AED7DFED0264A /* Pods-RunnerTests.debug.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/image_puzzle.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/image_puzzle";
+			};
+			name = Debug;
+		};
+		331C80DC294CF71000263BE5 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 6387C011E3E4A1492C644D22 /* Pods-RunnerTests.release.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/image_puzzle.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/image_puzzle";
+			};
+			name = Release;
+		};
+		331C80DD294CF71000263BE5 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 64F71DF4CBBB3DA6EA5564CE /* Pods-RunnerTests.profile.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.imagePuzzle.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/image_puzzle.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/image_puzzle";
+			};
+			name = Profile;
+		};
+		338D0CE9231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				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_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEAD_CODE_STRIPPING = YES;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = macosx;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+			};
+			name = Profile;
+		};
+		338D0CEA231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Profile;
+		};
+		338D0CEB231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Manual;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Profile;
+		};
+		33CC10F92044A3C60003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				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_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEAD_CODE_STRIPPING = YES;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		33CC10FA2044A3C60003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				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_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEAD_CODE_STRIPPING = YES;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = macosx;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+			};
+			name = Release;
+		};
+		33CC10FC2044A3C60003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		33CC10FD2044A3C60003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+		33CC111C2044C6BA0003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Manual;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		33CC111D2044C6BA0003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				331C80DB294CF71000263BE5 /* Debug */,
+				331C80DC294CF71000263BE5 /* Release */,
+				331C80DD294CF71000263BE5 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC10F92044A3C60003C045 /* Debug */,
+				33CC10FA2044A3C60003C045 /* Release */,
+				338D0CE9231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC10FC2044A3C60003C045 /* Debug */,
+				33CC10FD2044A3C60003C045 /* Release */,
+				338D0CEA231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC111C2044C6BA0003C045 /* Debug */,
+				33CC111D2044C6BA0003C045 /* Release */,
+				338D0CEB231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}

+ 8 - 0
macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 99 - 0
macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1510"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+               BuildableName = "image_puzzle.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">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "image_puzzle.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "331C80D4294CF70F00263BE5"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      enableGPUValidationMode = "1"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "image_puzzle.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "image_puzzle.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 10 - 0
macos/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>

+ 8 - 0
macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 13 - 0
macos/Runner/AppDelegate.swift

@@ -0,0 +1,13 @@
+import Cocoa
+import FlutterMacOS
+
+@main
+class AppDelegate: FlutterAppDelegate {
+  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+    return true
+  }
+
+  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+    return true
+  }
+}

+ 68 - 0
macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,68 @@
+{
+  "images" : [
+    {
+      "size" : "16x16",
+      "idiom" : "mac",
+      "filename" : "app_icon_16.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "16x16",
+      "idiom" : "mac",
+      "filename" : "app_icon_32.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "32x32",
+      "idiom" : "mac",
+      "filename" : "app_icon_32.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "32x32",
+      "idiom" : "mac",
+      "filename" : "app_icon_64.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "128x128",
+      "idiom" : "mac",
+      "filename" : "app_icon_128.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "128x128",
+      "idiom" : "mac",
+      "filename" : "app_icon_256.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "256x256",
+      "idiom" : "mac",
+      "filename" : "app_icon_256.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "256x256",
+      "idiom" : "mac",
+      "filename" : "app_icon_512.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "512x512",
+      "idiom" : "mac",
+      "filename" : "app_icon_512.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "512x512",
+      "idiom" : "mac",
+      "filename" : "app_icon_1024.png",
+      "scale" : "2x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png


BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä