From 5998044f2b0ef206a43d80fdb64a1acd0830bb85 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 09:57:05 +0100 Subject: [PATCH] Initialize generic Eve Flutter runtime --- .gitignore | 46 + .metadata | 39 + README.md | 34 + analysis_options.yaml | 28 + android/.gitignore | 14 + android/app/build.gradle.kts | 45 + android/app/src/debug/AndroidManifest.xml | 7 + android/app/src/main/AndroidManifest.xml | 45 + .../com/example/eve_parity/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + android/app/src/main/res/values/styles.xml | 18 + android/app/src/profile/AndroidManifest.xml | 7 + android/build.gradle.kts | 24 + android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + android/settings.gradle.kts | 26 + eveflutter-lifecycle.json | 56 + ios/.gitignore | 34 + ios/Flutter/AppFrameworkInfo.plist | 24 + ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Runner.xcodeproj/project.pbxproj | 644 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 119 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + ios/Runner/Base.lproj/LaunchScreen.storyboard | 37 + ios/Runner/Base.lproj/Main.storyboard | 26 + ios/Runner/Info.plist | 70 + ios/Runner/Runner-Bridging-Header.h | 1 + ios/Runner/SceneDelegate.swift | 6 + ios/RunnerTests/RunnerTests.swift | 12 + lib/main.dart | 1512 +++++++++++++++++ linux/.gitignore | 1 + linux/CMakeLists.txt | 128 ++ linux/flutter/CMakeLists.txt | 88 + linux/flutter/generated_plugin_registrant.cc | 11 + linux/flutter/generated_plugin_registrant.h | 15 + linux/flutter/generated_plugins.cmake | 23 + linux/runner/CMakeLists.txt | 26 + linux/runner/main.cc | 6 + linux/runner/my_application.cc | 148 ++ linux/runner/my_application.h | 21 + pubspec.lock | 205 +++ pubspec.yaml | 93 + scripts/capture-android-flutter-parity.ps1 | 80 + scripts/capture-android-screenshot.ps1 | 102 ++ scripts/capture-flutter-parity.ps1 | 54 + scripts/capture-linux-flutter-parity.ps1 | 68 + scripts/install-nightwing-flutter-sdk.ps1 | 17 + scripts/install-nightwing-flutter-sdk.sh | 33 + ...-eveflutter-conformance-consumer-smoke.ps1 | 57 + scripts/run-eveflutter-lifecycle-smoke.ps1 | 86 + test/goldens/cultui-inspector-desktop.png | Bin 0 -> 13323 bytes test/goldens/cultui-inspector-phone.png | Bin 0 -> 10746 bytes test/goldens/cultui-inspector-tablet.png | Bin 0 -> 12589 bytes .../gamecult.eve.embedded-demo-phone.png | Bin 0 -> 27677 bytes test/parity_smoke_test.dart | 277 +++ windows/.gitignore | 17 + windows/CMakeLists.txt | 108 ++ windows/flutter/CMakeLists.txt | 109 ++ .../flutter/generated_plugin_registrant.cc | 11 + windows/flutter/generated_plugin_registrant.h | 15 + windows/flutter/generated_plugins.cmake | 23 + windows/runner/CMakeLists.txt | 40 + windows/runner/Runner.rc | 121 ++ windows/runner/flutter_window.cpp | 71 + windows/runner/flutter_window.h | 33 + windows/runner/main.cpp | 43 + windows/runner/resource.h | 16 + windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes windows/runner/runner.exe.manifest | 14 + windows/runner/utils.cpp | 69 + windows/runner/utils.h | 19 + windows/runner/win32_window.cpp | 288 ++++ windows/runner/win32_window.h | 102 ++ 108 files changed, 5784 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/example/eve_parity/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values-night/styles.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle.kts create mode 100644 eveflutter-lifecycle.json create mode 100644 ios/.gitignore create mode 100644 ios/Flutter/AppFrameworkInfo.plist create mode 100644 ios/Flutter/Debug.xcconfig create mode 100644 ios/Flutter/Release.xcconfig create mode 100644 ios/Runner.xcodeproj/project.pbxproj create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner/AppDelegate.swift create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 ios/Runner/Base.lproj/Main.storyboard create mode 100644 ios/Runner/Info.plist create mode 100644 ios/Runner/Runner-Bridging-Header.h create mode 100644 ios/Runner/SceneDelegate.swift create mode 100644 ios/RunnerTests/RunnerTests.swift create mode 100644 lib/main.dart create mode 100644 linux/.gitignore create mode 100644 linux/CMakeLists.txt create mode 100644 linux/flutter/CMakeLists.txt create mode 100644 linux/flutter/generated_plugin_registrant.cc create mode 100644 linux/flutter/generated_plugin_registrant.h create mode 100644 linux/flutter/generated_plugins.cmake create mode 100644 linux/runner/CMakeLists.txt create mode 100644 linux/runner/main.cc create mode 100644 linux/runner/my_application.cc create mode 100644 linux/runner/my_application.h create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 scripts/capture-android-flutter-parity.ps1 create mode 100644 scripts/capture-android-screenshot.ps1 create mode 100644 scripts/capture-flutter-parity.ps1 create mode 100644 scripts/capture-linux-flutter-parity.ps1 create mode 100644 scripts/install-nightwing-flutter-sdk.ps1 create mode 100644 scripts/install-nightwing-flutter-sdk.sh create mode 100644 scripts/run-eveflutter-conformance-consumer-smoke.ps1 create mode 100644 scripts/run-eveflutter-lifecycle-smoke.ps1 create mode 100644 test/goldens/cultui-inspector-desktop.png create mode 100644 test/goldens/cultui-inspector-phone.png create mode 100644 test/goldens/cultui-inspector-tablet.png create mode 100644 test/goldens/gamecult.eve.embedded-demo-phone.png create mode 100644 test/parity_smoke_test.dart create mode 100644 windows/.gitignore create mode 100644 windows/CMakeLists.txt create mode 100644 windows/flutter/CMakeLists.txt create mode 100644 windows/flutter/generated_plugin_registrant.cc create mode 100644 windows/flutter/generated_plugin_registrant.h create mode 100644 windows/flutter/generated_plugins.cmake create mode 100644 windows/runner/CMakeLists.txt create mode 100644 windows/runner/Runner.rc create mode 100644 windows/runner/flutter_window.cpp create mode 100644 windows/runner/flutter_window.h create mode 100644 windows/runner/main.cpp create mode 100644 windows/runner/resource.h create mode 100644 windows/runner/resources/app_icon.ico create mode 100644 windows/runner/runner.exe.manifest create mode 100644 windows/runner/utils.cpp create mode 100644 windows/runner/utils.h create mode 100644 windows/runner/win32_window.cpp create mode 100644 windows/runner/win32_window.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cfaa9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# 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-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ +/assets/ + +# 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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..56e62b8 --- /dev/null +++ b/.metadata @@ -0,0 +1,39 @@ +# 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: "924134a44c189315be2148659913dda1671cbe99" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: android + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: ios + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: linux + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: windows + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + + # 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' diff --git a/README.md b/README.md new file mode 100644 index 0000000..bdaa4c6 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# EveFlutter + +Generic Flutter lowering for Eve surface documents. EveFlutter owns native +Flutter projection, provider selection, command-intent construction, platform +build glue, tests, and captures. Providers retain state and command authority; +plugins retain domain semantics. + +Current smoke target: + +- `CultUiInspectorSurface` +- custom-painted slider track/fill/thumb +- `EmbeddedSurfaceDemo`, which verifies `surface.slot` / + `embeddedDocuments` parsing and rendering from the shared fixture +- golden PNG output through `flutter test --update-goldens` + +Run from the repository root: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\capture-flutter-parity.ps1 -Target windows -SurfacePath +``` + +Linux capture requires running the same harness on a Linux host. The Windows +host must not pretend to be a Linux renderer. + +Split lifecycle evidence lives in `eveflutter-lifecycle.json`. From the +repository root, run: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\run-eveflutter-lifecycle-smoke.ps1 +``` + +That smoke validates the owner lifecycle document and runs the Dart analyzer. +Pass an exported conformance directory and Eve's generic export consumer to +verify contract consumption without importing Eve source layout. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/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 diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..36062f3 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.eve_parity" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.eve_parity" + // 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") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9111a10 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/eve_parity/MainActivity.kt b/android/app/src/main/kotlin/com/example/eve_parity/MainActivity.kt new file mode 100644 index 0000000..047e060 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/eve_parity/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.eve_parity + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/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-9.1.0-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +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 "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/eveflutter-lifecycle.json b/eveflutter-lifecycle.json new file mode 100644 index 0000000..2e25c56 --- /dev/null +++ b/eveflutter-lifecycle.json @@ -0,0 +1,56 @@ +{ + "schema": "gamecult.eve.runtime_lifecycle.v1", + "splitTarget": "EveFlutter", + "ownerRepo": "EveFlutter", + "currentHostRepo": "EveFlutter", + "status": "owner-hosted", + "runtimes": [ + "windows-flutter", + "linux-flutter", + "android-flutter" + ], + "release": { + "ownerRepo": "EveFlutter", + "status": "incubating-flutter-app", + "evidencePaths": [ + "pubspec.yaml", + "README.md" + ], + "pendingProofs": [ + "Tagged EveFlutter release is cut from EveFlutter rather than Eve incubation." + ] + }, + "test": { + "ownerRepo": "EveFlutter", + "status": "dart-analyze-and-widget-smoke", + "commands": [ + "dart analyze lib test", + "powershell -ExecutionPolicy Bypass -File scripts/run-eveflutter-conformance-consumer-smoke.ps1" + ], + "evidencePaths": [ + "lib/main.dart", + "test/parity_smoke_test.dart", + "scripts/run-eveflutter-conformance-consumer-smoke.ps1" + ], + "pendingProofs": [ + "Tagged EveFlutter release runs owner-repo tests." + ] + }, + "capture": { + "ownerRepo": "EveFlutter", + "status": "windows-linux-android-capture-smokes", + "evidencePaths": [ + "scripts/capture-flutter-parity.ps1", + "scripts/capture-linux-flutter-parity.ps1", + "scripts/capture-android-flutter-parity.ps1" + ], + "pendingProofs": [ + "Native capture witnesses are attached from EveFlutter on each supported platform." + ] + }, + "remainingSplitBlockers": [ + "Tagged EveFlutter release is cut from EveFlutter rather than Eve incubation.", + "Tagged EveFlutter release runs owner-repo tests.", + "Native capture witnesses are attached from EveFlutter on each supported platform." + ] +} diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/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 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1573b41 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*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 */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* 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; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + 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; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + 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 */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 = 13.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.eveParity; + 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.eveParity.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.eveParity.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.eveParity.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 = 13.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 = 13.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.eveParity; + 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.eveParity; + 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 */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/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" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/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" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..01c7b8b --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Eve Parity + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + eve_parity + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/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. + } + +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..8c4a1c4 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,1512 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() { + runApp(const EveParityApp()); +} + +typedef EveCommandSink = void Function(EveCommandIntent intent, EveNode node); + +class EveParityApp extends StatefulWidget { + const EveParityApp({ + super.key, + this.assetPath = 'assets/current-surface.json', + this.commandSink, + }); + + final String assetPath; + final EveCommandSink? commandSink; + + @override + State createState() => _EveParityAppState(); +} + +class _EveParityAppState extends State { + late Future surface; + + @override + void initState() { + super.initState(); + surface = EveSurfaceState.load(widget.assetPath); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: FutureBuilder( + future: surface, + builder: (context, snapshot) { + final state = snapshot.data; + final tokens = state?.tokens ?? EveTokens.defaults; + return Scaffold( + backgroundColor: tokens.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: snapshot.hasError + ? Text( + 'Eve surface load failed\n${snapshot.error}', + style: const TextStyle( + color: Color(0xFFFFB84F), + fontFamily: 'monospace', + ), + ) + : state == null + ? const SizedBox.shrink() + : SingleChildScrollView( + child: EveSurfaceView( + state: state, + commandSink: widget.commandSink, + ), + ), + ), + ), + ); + }, + ), + ); + } +} + +class EveSurfaceState { + EveSurfaceState({ + required this.providerId, + required this.surfaceId, + required this.title, + required this.root, + required this.tokens, + required this.controlSkins, + required this.values, + }); + + final String providerId; + final String surfaceId; + final String title; + final EveNode root; + final EveTokens tokens; + final Map> controlSkins; + final Map values; + + static Future load(String assetPath) async { + final raw = await rootBundle.loadString(assetPath); + final json = jsonDecode(raw) as Map; + return EveSurfaceState.fromJson(json); + } + + factory EveSurfaceState.fromJson(Map json) { + final surface = json['surface'] as Map; + final styles = (surface['styles'] as Map?) ?? const {}; + final skins = >{}; + final rawSkins = + (styles['controlSkins'] as Map?) ?? const {}; + for (final entry in rawSkins.entries) { + final skin = entry.value as Map; + skins[entry.key] = ((skin['children'] as List?) ?? const []) + .map((child) => EveNode.fromJson(child as Map)) + .toList(); + } + return EveSurfaceState( + providerId: (json['providerId'] ?? '').toString(), + surfaceId: (surface['id'] ?? '').toString(), + title: (json['title'] ?? '').toString(), + root: EveNode.fromJson(surface['root'] as Map), + tokens: EveTokens.fromJson( + (styles['tokens'] as Map?) ?? const {}, + ), + controlSkins: skins, + values: (json['values'] as Map?) ?? const {}, + ); + } +} + +class EveProviderCatalog { + const EveProviderCatalog({required this.providers}); + + final List providers; + + factory EveProviderCatalog.fromConformanceExport(Map json) { + final providers = ((json['providers'] as List?) ?? const []) + .map((provider) { + return EveProviderEntry.fromJson(provider as Map); + }) + .toList(growable: false); + return EveProviderCatalog(providers: providers); + } + + EveProviderEntry? findProvider(String providerId) { + for (final provider in providers) { + if (provider.providerId == providerId) return provider; + } + return null; + } +} + +class EveProviderEntry { + const EveProviderEntry({ + required this.providerId, + required this.status, + required this.ownerRepo, + required this.advertisementPath, + required this.surfaces, + required this.commands, + required this.pluginRequirements, + }); + + final String providerId; + final String status; + final String ownerRepo; + final String advertisementPath; + final List surfaces; + final List commands; + final List pluginRequirements; + + factory EveProviderEntry.fromJson(Map json) { + return EveProviderEntry( + providerId: (json['providerId'] ?? '').toString(), + status: (json['status'] ?? '').toString(), + ownerRepo: (json['ownerRepo'] ?? '').toString(), + advertisementPath: (json['advertisementPath'] ?? '').toString(), + surfaces: _stringList(json['surfaces']), + commands: _stringList(json['commands']), + pluginRequirements: + ((json['pluginRequirements'] as List?) ?? const []) + .map( + (requirement) => EvePluginRequirement.fromJson( + requirement as Map, + ), + ) + .toList(growable: false), + ); + } +} + +class EvePluginRequirement { + const EvePluginRequirement({ + required this.surfaceId, + required this.pluginId, + required this.requiredCapabilities, + }); + + final String surfaceId; + final String pluginId; + final List requiredCapabilities; + + factory EvePluginRequirement.fromJson(Map json) { + return EvePluginRequirement( + surfaceId: (json['surfaceId'] ?? '').toString(), + pluginId: (json['pluginId'] ?? '').toString(), + requiredCapabilities: _stringList(json['requiredCapabilities']), + ); + } +} + +List _stringList(Object? value) { + return ((value as List?) ?? const []) + .map((item) => item.toString()) + .toList(growable: false); +} + +class EveProviderPicker extends StatelessWidget { + const EveProviderPicker({ + required this.catalog, + required this.selectedProviderId, + required this.onSelected, + super.key, + }); + + final EveProviderCatalog catalog; + final String selectedProviderId; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + return DropdownButton( + value: catalog.findProvider(selectedProviderId) == null + ? null + : selectedProviderId, + hint: const Text('Select provider'), + items: [ + for (final provider in catalog.providers) + DropdownMenuItem( + value: provider.providerId, + child: Text(provider.providerId), + ), + ], + onChanged: (value) { + if (value != null) onSelected(value); + }, + ); + } +} + +class EveNode { + EveNode({ + required this.id, + required this.kind, + required this.props, + required this.layout, + required this.children, + required this.embeddedDocuments, + }); + + final String id; + final String kind; + final Map props; + final Map layout; + final List children; + final List embeddedDocuments; + + factory EveNode.fromJson(Map json) { + return EveNode( + id: (json['id'] ?? '').toString(), + kind: (json['kind'] ?? 'panel').toString(), + props: (json['props'] as Map?) ?? const {}, + layout: (json['layout'] as Map?) ?? const {}, + children: ((json['children'] as List?) ?? const []) + .map((child) => EveNode.fromJson(child as Map)) + .toList(), + embeddedDocuments: + ((json['embeddedDocuments'] as List?) ?? const []) + .map( + (slot) => EveEmbeddedDocumentSlot.fromJson( + slot as Map, + ), + ) + .toList(), + ); + } +} + +class EveCommandIntent { + EveCommandIntent({ + required this.type, + required this.schema, + required this.providerId, + required this.surfaceId, + required this.command, + required this.commandId, + required this.clientId, + required this.issuedAtUtc, + required this.payload, + }); + + final String type; + final String schema; + final String providerId; + final String surfaceId; + final String command; + final String commandId; + final String clientId; + final String issuedAtUtc; + final Map payload; + + Map toJson() => { + 'type': type, + 'schema': schema, + 'providerId': providerId, + 'surfaceId': surfaceId, + 'command': command, + 'commandId': commandId, + 'clientId': clientId, + 'issuedAtUtc': issuedAtUtc, + 'payload': payload, + }; +} + +EveCommandIntent createEveCommandIntent( + EveSurfaceState state, + EveNode node, + String command, +) { + return EveCommandIntent( + type: 'surface-command', + schema: 'gamecult.eve.command_invocation.v1', + providerId: state.providerId, + surfaceId: state.surfaceId, + command: command, + commandId: node.props['commandId']?.toString() ?? command, + clientId: 'flutter-parity', + issuedAtUtc: DateTime.now().toUtc().toIso8601String(), + payload: { + 'nodeId': node.id, + 'kind': node.kind, + 'props': node.props, + if (node.props['action'] != null) 'action': node.props['action'], + }, + ); +} + +String resolveComponentCommandId(Map props, EveNode node) { + final action = props['action'] is Map + ? props['action'] as Map + : const {}; + for (final value in [ + props['command'], + action['command'], + action['target'], + action['type'], + props['commandId'], + node.id, + ]) { + if (value is String && value.trim().isNotEmpty) return value; + } + return ''; +} + +class EveEmbeddedDocumentSlot { + EveEmbeddedDocumentSlot({ + required this.slotId, + required this.documentId, + required this.schemaId, + required this.presentationKind, + }); + + final String slotId; + final String documentId; + final String schemaId; + final String presentationKind; + + factory EveEmbeddedDocumentSlot.fromJson(Map json) { + return EveEmbeddedDocumentSlot( + slotId: (json['slotId'] ?? '').toString(), + documentId: (json['documentId'] ?? '').toString(), + schemaId: (json['schemaId'] ?? '').toString(), + presentationKind: (json['presentationKind'] ?? '').toString(), + ); + } +} + +class EveTokens { + const EveTokens({ + required this.background, + required this.panel, + required this.panelAlt, + required this.panelInset, + required this.border, + required this.text, + required this.muted, + required this.accent, + }); + + final Color background; + final Color panel; + final Color panelAlt; + final Color panelInset; + final Color border; + final Color text; + final Color muted; + final Color accent; + + static const defaults = EveTokens( + background: Color(0xFF020909), + panel: Color(0xFF071918), + panelAlt: Color(0xFF15151D), + panelInset: Color(0x57000000), + border: Color(0x4767F0E4), + text: Color(0xFFE7F1F1), + muted: Color(0xFF8BA5A3), + accent: Color(0xFFFFB84F), + ); + + factory EveTokens.fromJson(Map json) { + return EveTokens( + background: _color(json['colorBackground'], defaults.background), + panel: _color(json['colorPanel'], defaults.panel), + panelAlt: _color(json['colorPanelAlt'], defaults.panelAlt), + panelInset: _color(json['colorPanelInset'], defaults.panelInset), + border: _color(json['colorPanelBorder'], defaults.border), + text: _color(json['colorText'], defaults.text), + muted: _color(json['colorMuted'], defaults.muted), + accent: _color(json['colorAccent'], defaults.accent), + ); + } + + static Color _color(Object? value, Color fallback) { + if (value is! String) return fallback; + final hex = value.trim(); + if (!hex.startsWith('#')) return fallback; + final body = hex.substring(1); + if (body.length == 6) return Color(int.parse('ff$body', radix: 16)); + if (body.length == 8) return Color(int.parse(body, radix: 16)); + return fallback; + } +} + +class EveSurfaceView extends StatelessWidget { + const EveSurfaceView({required this.state, this.commandSink, super.key}); + + final EveSurfaceState state; + final EveCommandSink? commandSink; + + @override + Widget build(BuildContext context) { + return EveNodeView( + state: state, + node: state.root, + commandSink: commandSink, + ); + } +} + +class EveNodeView extends StatelessWidget { + const EveNodeView({ + required this.state, + required this.node, + this.commandSink, + super.key, + }); + + final EveSurfaceState state; + final EveNode node; + final EveCommandSink? commandSink; + + @override + Widget build(BuildContext context) { + switch (node.kind) { + case 'surface': + return _surface(); + case 'partition': + return _partition(); + case 'pane': + case 'panel': + return _pane(); + case 'card': + case 'card.external': + return _card(); + case 'label': + case 'text': + case 'text.title': + case 'text.dialogue': + return _text(); + case 'control.slider': + return EveSlider(state: state, node: node); + case 'control.button': + return _button(); + case 'image.preview': + return _imagePreview(); + case 'canvas.preview': + case 'canvas.editor': + return _canvasPlaceholder(editor: node.kind == 'canvas.editor'); + case 'status.stage': + return _statusStage(); + case 'input.file': + case 'dropzone': + return _button(); + case 'input.number': + case 'input.select': + case 'control.range': + return _field(); + case 'control.toggle': + return _toggle(); + case 'color.swatch': + return _swatch(); + case 'vn.stage': + return _vnStage(); + case 'image.background': + return _background(); + case 'embed.norn': + return _norn(); + case 'embed.tex': + return _tex(); + case 'panel.dialogue': + return _dialogue(); + case 'rail.actions': + return _actions(); + case 'surface.slot': + return _surfaceSlot(); + default: + return _fallback(); + } + } + + Widget _surfaceSlot() { + final slot = node.embeddedDocuments.firstOrNull; + final label = + node.props['presentationKind']?.toString() ?? + slot?.presentationKind ?? + node.props['slotId']?.toString() ?? + slot?.slotId ?? + 'embedded surface'; + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panelAlt.withValues(alpha: 0.65), + border: Border.all(color: state.tokens.accent.withValues(alpha: 0.45)), + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + label, + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + ), + ), + ), + ); + } + + Widget _vnStage() { + final background = node.children + .where((child) => child.kind == 'image.background') + .firstOrNull; + final sceneChildren = node.children.where((child) { + return child.kind != 'image.background' && + child.kind != 'panel.dialogue' && + child.kind != 'rail.actions'; + }).toList(); + final dialogue = node.children + .where((child) => child.kind == 'panel.dialogue') + .firstOrNull; + final actions = node.children + .where((child) => child.kind == 'rail.actions') + .firstOrNull; + + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final height = constraints.maxHeight.isFinite + ? constraints.maxHeight + : width * 0.56; + return SizedBox( + width: width, + height: height.clamp(520, 900), + child: Stack( + children: [ + Positioned.fill( + child: background == null + ? DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.background, + ), + ) + : EveNodeView( + state: state, + node: background, + commandSink: commandSink, + ), + ), + for (final child in sceneChildren) _stageOverlay(child), + if (dialogue != null) + Positioned( + left: 28, + right: 28, + bottom: actions == null ? 24 : 96, + child: EveNodeView( + state: state, + node: dialogue, + commandSink: commandSink, + ), + ), + if (actions != null) + Positioned( + left: 28, + right: 28, + bottom: 24, + child: EveNodeView( + state: state, + node: actions, + commandSink: commandSink, + ), + ), + ], + ), + ); + }, + ); + } + + Positioned _stageOverlay(EveNode child) { + if (child.kind == 'layer.embedded-surfaces' || + child.kind == 'layer.cards') { + return Positioned.fill( + child: IgnorePointer( + child: Stack( + children: [ + for (final grandchild in child.children) + _stageOverlay(grandchild), + ], + ), + ), + ); + } + final placement = _map(child.props['placement']); + final anchor = placement['anchor']?.toString(); + if (child.kind == 'embed.norn' || anchor == 'whiteboard') { + return Positioned( + left: 54, + top: 46, + width: 520, + height: 310, + child: EveNodeView(state: state, node: child, commandSink: commandSink), + ); + } + if (child.kind == 'embed.tex' || anchor == 'whiteboard-equation') { + return Positioned( + left: 96, + top: 350, + width: 500, + height: 94, + child: EveNodeView(state: state, node: child, commandSink: commandSink), + ); + } + return Positioned( + right: 32, + top: 52, + width: 300, + child: EveNodeView(state: state, node: child, commandSink: commandSink), + ); + } + + Widget _background() { + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.background, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + state.tokens.panelAlt, + state.tokens.background, + const Color(0xFF10161F), + ], + ), + ), + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: EveGridPainter( + color: state.tokens.accent.withValues(alpha: 0.10), + ), + ), + ), + Positioned( + left: 28, + top: 24, + child: Text( + node.props['label']?.toString() ?? 'Scene', + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ); + } + + Widget _norn() { + final graph = _map(node.props['graph']); + final nodes = (graph['nodes'] as List?) ?? const []; + final edges = (graph['edges'] as List?) ?? const []; + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panel.withValues(alpha: 0.82), + border: Border.all(color: state.tokens.border), + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: CustomPaint( + painter: EveNornPainter( + nodes: nodes, + edges: edges, + tokens: state.tokens, + ), + child: const SizedBox.expand(), + ), + ), + ); + } + + Widget _tex() { + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panel.withValues(alpha: 0.88), + border: Border.all(color: state.tokens.border), + borderRadius: BorderRadius.circular(5), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (node.props['label'] != null) + Text( + node.props['label'].toString().toUpperCase(), + style: TextStyle( + color: state.tokens.accent, + fontFamily: 'EveParity', + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + Text( + node.props['source']?.toString() ?? '', + style: TextStyle( + color: state.tokens.text, + fontFamily: 'EveParity', + fontSize: 18, + ), + ), + ], + ), + ), + ); + } + + Widget _dialogue() { + final speaker = node.props['speaker']?.toString() ?? ''; + final text = node.props['text']?.toString() ?? ''; + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panel.withValues(alpha: 0.94), + border: Border.all(color: state.tokens.border), + borderRadius: BorderRadius.circular(8), + boxShadow: const [ + BoxShadow( + color: Color(0x8A000000), + blurRadius: 24, + offset: Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + speaker, + style: TextStyle( + color: state.tokens.accent, + fontFamily: 'EveParity', + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + Text( + text, + style: TextStyle( + color: state.tokens.text, + fontFamily: 'EveParity', + fontSize: 20, + ), + ), + ], + ), + ), + ); + } + + Widget _actions() { + return Row( + children: [ + for (var index = 0; index < node.children.length; index += 1) ...[ + Expanded( + child: EveNodeView( + state: state, + node: node.children[index], + commandSink: commandSink, + ), + ), + if (index != node.children.length - 1) const SizedBox(width: 10), + ], + ], + ); + } + + Widget _button() { + final label = + node.props['label']?.toString() ?? node.props['text']?.toString() ?? ''; + final command = resolveComponentCommandId(node.props, node); + final body = DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.accent.withValues(alpha: 0.17), + border: Border.all(color: state.tokens.accent.withValues(alpha: 0.64)), + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: state.tokens.text, + fontFamily: 'EveParity', + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + ), + ); + if (command.isEmpty || commandSink == null) return body; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => + commandSink?.call(createEveCommandIntent(state, node, command), node), + child: body, + ); + } + + Widget _imagePreview() { + final label = node.props['label']?.toString() ?? 'Image'; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 1, + child: DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panelInset, + border: Border.all(color: state.tokens.border, width: 2), + ), + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: EveGridPainter( + color: state.tokens.accent.withValues(alpha: 0.10), + ), + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + Text( + label, + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + ), + ), + ], + ); + } + + Widget _canvasPlaceholder({required bool editor}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + node.props['label']?.toString() ?? 'Canvas', + style: TextStyle( + color: state.tokens.accent, + fontFamily: 'EveParity', + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + SizedBox( + height: editor ? 280 : 170, + width: double.infinity, + child: DecoratedBox( + decoration: BoxDecoration( + color: editor ? const Color(0xFF142033) : state.tokens.panelInset, + border: Border.all(color: state.tokens.border, width: 2), + ), + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: EveGridPainter( + color: state.tokens.accent.withValues( + alpha: editor ? 0.18 : 0.08, + ), + ), + ), + ), + Center( + child: Text( + node.props['state']?.toString() ?? '', + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _statusStage() { + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panelAlt, + border: Border.all(color: state.tokens.border, width: 2), + borderRadius: BorderRadius.circular(4), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + node.props['label']?.toString() ?? 'Status', + style: TextStyle( + color: state.tokens.accent, + fontFamily: 'EveParity', + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 6), + Text( + node.props['stage']?.toString() ?? '', + style: TextStyle( + color: state.tokens.text, + fontFamily: 'EveParity', + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + node.props['detail']?.toString() ?? '', + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + ), + ), + ], + ), + ), + ); + } + + Widget _field() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + node.props['label']?.toString() ?? '', + style: TextStyle( + color: state.tokens.accent, + fontFamily: 'EveParity', + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panelInset, + border: Border.all(color: state.tokens.border), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Text( + node.props['value']?.toString() ?? '', + style: TextStyle( + color: state.tokens.text, + fontFamily: 'EveParity', + fontSize: 13, + ), + ), + ), + ), + ], + ); + } + + Widget _toggle() { + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panelInset, + border: Border.all(color: state.tokens.border), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + '${node.props['label'] ?? 'Toggle'}: ${node.props['value'] == true ? 'on' : 'off'}', + style: TextStyle( + color: state.tokens.text, + fontFamily: 'EveParity', + fontSize: 12, + ), + ), + ), + ); + } + + Widget _swatch() { + return Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: _parseColor(node.props['value']) ?? state.tokens.text, + border: Border.all(color: state.tokens.accent, width: 2), + ), + ); + } + + Widget _surface() { + final padding = _number(node.layout['padding'] ?? node.props['padding'], 0); + final direction = node.layout['direction']?.toString(); + return Padding( + padding: EdgeInsets.all(padding), + child: LayoutBuilder( + builder: (context, constraints) { + if (direction == 'vertical') { + return _children(axis: Axis.vertical, maxWidth: 1280); + } + if (direction == 'horizontal') { + return _children(axis: Axis.horizontal, maxWidth: 1280); + } + if (constraints.maxWidth >= 900 && node.children.length > 1) { + return _children(axis: Axis.horizontal, maxWidth: 1280); + } + return _children(axis: Axis.vertical, maxWidth: 1180); + }, + ), + ); + } + + Widget _partition() { + final split = (node.props['split'] ?? _layoutDirection()).toString(); + final gap = _number(node.props['gap'] ?? node.layout['gap'], 0); + final padding = _number(node.props['padding'] ?? node.layout['padding'], 0); + final body = _children( + axis: split == 'x' ? Axis.horizontal : Axis.vertical, + ); + return Padding( + padding: EdgeInsets.all(padding), + child: split == 'x' + ? LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 520 && + node.props['role'] == 'inspector.row') { + return _column(gap); + } + return body; + }, + ) + : body, + ); + } + + Widget _pane() { + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panel, + border: Border.all(color: state.tokens.border), + borderRadius: BorderRadius.circular(6), + boxShadow: const [ + BoxShadow( + color: Color(0x59000000), + blurRadius: 26, + offset: Offset(0, 12), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(_number(node.props['padding'], 12)), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (node.props['title'] != null) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Text( + node.props['title'].toString().toUpperCase(), + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ), + ..._spacedChildren(Axis.vertical, _number(node.props['gap'], 0)), + ], + ), + ), + ); + } + + Widget _card() { + return DecoratedBox( + decoration: BoxDecoration( + color: state.tokens.panelAlt, + border: Border.all(color: const Color(0x10FFFFFF)), + borderRadius: BorderRadius.circular(4), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (node.props['title'] != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + node.props['title'].toString(), + style: TextStyle( + color: state.tokens.muted, + fontFamily: 'EveParity', + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ), + _children(axis: Axis.vertical), + ], + ), + ), + ); + } + + Widget _text() { + final bind = node.props['bind']?.toString(); + final value = bind == null ? node.props['text'] : state.values[bind]; + final text = + '${node.props['prefix'] ?? ''}${value ?? ''}${node.props['suffix'] ?? ''}'; + return Text( + node.kind == 'label' ? text.toUpperCase() : text, + style: TextStyle( + color: node.kind == 'label' ? state.tokens.accent : state.tokens.text, + fontFamily: 'EveParity', + fontSize: node.kind == 'text.title' ? 18 : 13, + fontWeight: node.kind == 'label' ? FontWeight.w700 : FontWeight.w500, + ), + ); + } + + Widget _fallback() { + return node.children.isEmpty + ? const SizedBox.shrink() + : _children(axis: Axis.vertical); + } + + Widget _children({required Axis axis, double? maxWidth}) { + final gap = _number(node.props['gap'], 0); + final childWidgets = _spacedChildren(axis, gap); + final content = axis == Axis.horizontal + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: childWidgets, + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: childWidgets, + ); + final constrained = maxWidth == null + ? content + : Align( + alignment: Alignment.topLeft, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: content, + ), + ); + return constrained; + } + + Widget _column(double gap) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _spacedChildren(Axis.vertical, gap), + ); + } + + List _spacedChildren(Axis axis, double gap) { + final widgets = []; + for (var index = 0; index < node.children.length; index += 1) { + final child = node.children[index]; + Widget view = EveNodeView( + state: state, + node: child, + commandSink: commandSink, + ); + if (axis == Axis.horizontal && _isFlexible(child)) { + view = Expanded(child: view); + } + widgets.add(view); + if (gap > 0 && index != node.children.length - 1) { + widgets.add( + axis == Axis.horizontal + ? SizedBox(width: gap) + : SizedBox(height: gap), + ); + } + } + return widgets; + } + + bool _isFlexible(EveNode child) { + final size = child.props['size']?.toString(); + return size == null || size.endsWith('fr'); + } + + String _layoutDirection() { + final direction = node.layout['direction']?.toString(); + if (direction == 'horizontal') { + return 'x'; + } + return 'y'; + } + + Map _map(Object? value) => + value is Map ? value : const {}; + + Color? _parseColor(Object? value) { + if (value is! String || !value.startsWith('#')) return null; + final body = value.substring(1); + if (body.length == 8) return Color(int.parse(body, radix: 16)); + if (body.length == 6) return Color(int.parse('ff$body', radix: 16)); + return null; + } +} + +class EveGridPainter extends CustomPainter { + EveGridPainter({required this.color}); + + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + for (var x = 0.0; x < size.width; x += 32) { + canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); + } + for (var y = 0.0; y < size.height; y += 32) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(EveGridPainter oldDelegate) => oldDelegate.color != color; +} + +class EveNornPainter extends CustomPainter { + EveNornPainter({ + required this.nodes, + required this.edges, + required this.tokens, + }); + + final List nodes; + final List edges; + final EveTokens tokens; + + @override + void paint(Canvas canvas, Size size) { + final nodeById = {}; + for (final raw in nodes) { + if (raw is! Map) continue; + final id = raw['id']?.toString(); + if (id == null) continue; + nodeById[id] = Offset( + _number(raw['x'], 0.5) * size.width, + _number(raw['y'], 0.5) * size.height, + ); + } + final edgePaint = Paint() + ..color = tokens.muted.withValues(alpha: 0.5) + ..strokeWidth = 2; + for (final raw in edges) { + if (raw is! Map) continue; + final source = nodeById[raw['source']?.toString()]; + final target = nodeById[raw['target']?.toString()]; + if (source != null && target != null) { + canvas.drawLine(source, target, edgePaint); + } + } + for (final raw in nodes) { + if (raw is! Map) continue; + final id = raw['id']?.toString(); + final center = id == null ? null : nodeById[id]; + if (center == null) continue; + final current = raw['current'] == true; + canvas.drawCircle( + center, + current ? 24 : 20, + Paint()..color = current ? tokens.accent : tokens.panelAlt, + ); + canvas.drawCircle( + center, + current ? 24 : 20, + Paint() + ..color = current ? tokens.accent : tokens.border + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + final label = raw['label']?.toString() ?? id; + final paragraph = TextPainter( + text: TextSpan( + text: label, + style: TextStyle( + color: tokens.text, + fontFamily: 'EveParity', + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + textDirection: TextDirection.ltr, + )..layout(maxWidth: 100); + paragraph.paint(canvas, center + Offset(-paragraph.width / 2, 30)); + } + } + + @override + bool shouldRepaint(EveNornPainter oldDelegate) => + oldDelegate.nodes != nodes || oldDelegate.edges != edges; +} + +class EveSlider extends StatelessWidget { + const EveSlider({required this.state, required this.node, super.key}); + + final EveSurfaceState state; + final EveNode node; + + @override + Widget build(BuildContext context) { + final bind = node.props['bind']?.toString(); + final min = _number(node.props['min'], 0); + final max = _number(node.props['max'], 1); + final rawValue = bind == null ? node.props['value'] : state.values[bind]; + final value = _number(rawValue, min).clamp(min, max); + final percent = max == min + ? 0.0 + : ((value - min) / (max - min)).clamp(0.0, 1.0); + final skin = state.controlSkins[node.props['skin']?.toString()] ?? const []; + final box = + skin.where((part) => part.kind == 'control.box').firstOrNull?.props ?? + const {}; + return CustomPaint( + painter: EveSliderPainter( + value: percent, + tokens: state.tokens, + skin: skin, + ), + child: SizedBox( + height: _number(box['height'], 22).clamp(18, 36), + width: double.infinity, + ), + ); + } +} + +class EveSliderPainter extends CustomPainter { + EveSliderPainter({ + required this.value, + required this.tokens, + required this.skin, + }); + + final double value; + final EveTokens tokens; + final List skin; + + @override + void paint(Canvas canvas, Size size) { + final centerY = size.height / 2; + final trackHeight = _partSize('track', 1, 6); + final thumbSize = _partSize('thumb', 0, 12); + final track = RRect.fromRectAndRadius( + Rect.fromLTWH(0, centerY - trackHeight / 2, size.width, trackHeight), + Radius.circular(_partRadius('track', 2)), + ); + final fill = RRect.fromRectAndRadius( + Rect.fromLTWH( + 0, + centerY - trackHeight / 2, + size.width * value, + trackHeight, + ), + Radius.circular(_partRadius('fill', 2)), + ); + + canvas.drawRRect(track, Paint()..color = tokens.panelInset); + canvas.drawRRect(fill, Paint()..color = tokens.accent); + final thumbCenter = Offset(size.width * value, centerY); + canvas.drawCircle( + thumbCenter, + thumbSize / 2 + _partBleed('thumb', 0), + Paint() + ..color = tokens.accent.withValues(alpha: 0.35) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5), + ); + canvas.drawCircle( + thumbCenter, + thumbSize / 2, + Paint()..color = tokens.accent, + ); + } + + double _partSize(String name, int index, double fallback) { + final part = _part(name); + final size = part?['size']; + if (size is List && size.length > index) { + return _number(size[index], fallback); + } + return fallback; + } + + double _partRadius(String name, double fallback) => + _number(_part(name)?['radius'], fallback); + double _partBleed(String name, double fallback) => + _number(_part(name)?['bleed'], fallback); + + Map? _part(String name) { + for (final node in skin) { + if (node.kind == 'control.part' && node.props['name'] == name) { + return node.props; + } + } + return null; + } + + @override + bool shouldRepaint(covariant EveSliderPainter oldDelegate) { + return oldDelegate.value != value || + oldDelegate.tokens != tokens || + oldDelegate.skin != skin; + } +} + +double _number(Object? value, double fallback) { + if (value is num) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? fallback; + return fallback; +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..666ba2a --- /dev/null +++ b/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 "eve_parity") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.eve_parity") + +# 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 "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>: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() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/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} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/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 + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +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 $) + 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) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/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}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/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); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..511b970 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#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) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// 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, "eve_parity"); + 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, "eve_parity"); + } + + gtk_window_set_default_size(window, 1280, 720); + + 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); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(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)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +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_ diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..2450bf3 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,205 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" +sdks: + dart: ">=3.12.1 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..a2fe3ea --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,93 @@ +name: eve_parity +description: "Eve Flutter parity renderer and screenshot smoke target." +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.12.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + assets: + - assets/current-surface.json + + fonts: + - family: EveParity + fonts: + - asset: assets/fonts/Roboto-Regular.ttf + - asset: assets/fonts/Roboto-Bold.ttf + weight: 700 + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/scripts/capture-android-flutter-parity.ps1 b/scripts/capture-android-flutter-parity.ps1 new file mode 100644 index 0000000..ecbd41b --- /dev/null +++ b/scripts/capture-android-flutter-parity.ps1 @@ -0,0 +1,80 @@ +param( + [string] $FixtureId = "cultui-inspector", + [Parameter(Mandatory = $true)] + [string] $SurfacePath, + [string] $FlutterExe = "E:\Projects\Eve\tools\deps\flutter\bin\flutter.bat", + [string] $OutputPath = "artifacts\parity\android-flutter-cultui-inspector.png", + [int] $Width = 0, + [int] $Height = 0, + [ValidateSet("portrait", "landscape", "natural")] + [string] $Orientation = "natural", + [int] $LaunchDelaySeconds = 4, + [switch] $SkipBuild, + [switch] $ForceInstall +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$flutterRoot = $projectRoot +$flutter = $FlutterExe +$flutterSdkRoot = Split-Path -Parent (Split-Path -Parent $flutter) +$assetPath = Join-Path $flutterRoot "assets\current-surface.json" +$fontDir = Join-Path $flutterRoot "assets\fonts" +$apkPath = Join-Path $flutterRoot "build\app\outputs\flutter-apk\app-debug.apk" + +if (-not (Test-Path $flutter)) { + throw "Flutter is not installed at $flutter" +} + +if (-not $SkipBuild) { + New-Item -ItemType Directory -Force (Split-Path -Parent $assetPath) | Out-Null + New-Item -ItemType Directory -Force $fontDir | Out-Null + Copy-Item -LiteralPath (Join-Path $flutterSdkRoot "bin\cache\artifacts\material_fonts\roboto-regular.ttf") -Destination (Join-Path $fontDir "Roboto-Regular.ttf") -Force + Copy-Item -LiteralPath (Join-Path $flutterSdkRoot "bin\cache\artifacts\material_fonts\roboto-bold.ttf") -Destination (Join-Path $fontDir "Roboto-Bold.ttf") -Force + Copy-Item -LiteralPath $SurfacePath -Destination $assetPath -Force + + Push-Location $flutterRoot + try { + & $flutter pub get + if ($LASTEXITCODE -ne 0) { + throw "Flutter pub get failed with exit code $LASTEXITCODE" + } + & $flutter build apk --debug "--dart-define=EVE_PARITY_FIXTURE=$FixtureId" + if ($LASTEXITCODE -ne 0) { + throw "Flutter Android APK build failed with exit code $LASTEXITCODE" + } + } finally { + Pop-Location + } +} + +if (-not (Test-Path $apkPath)) { + throw "Flutter Android APK not found: $apkPath" +} + +$captureArgs = @( + "-ExecutionPolicy", "Bypass", + "-File", ".\scripts\capture-android-screenshot.ps1", + "-OutputPath", $OutputPath, + "-ApkPath", $apkPath, + "-PackageName", "com.example.eve_parity", + "-ActivityName", ".MainActivity", + "-LaunchDelaySeconds", $LaunchDelaySeconds, + "-Orientation", $Orientation +) + +if ($Width -gt 0) { + $captureArgs += @("-Width", $Width) +} +if ($Height -gt 0) { + $captureArgs += @("-Height", $Height) +} +if ($ForceInstall -or -not $SkipBuild) { + $captureArgs += "-ForceInstall" +} + +powershell @captureArgs +if ($LASTEXITCODE -ne 0) { + throw "Android Flutter screenshot failed with exit code $LASTEXITCODE" +} diff --git a/scripts/capture-android-screenshot.ps1 b/scripts/capture-android-screenshot.ps1 new file mode 100644 index 0000000..c77a0f7 --- /dev/null +++ b/scripts/capture-android-screenshot.ps1 @@ -0,0 +1,102 @@ +param( + [string] $OutputPath = "artifacts\parity\android-periwinkle.png", + [string] $ApkPath = "artifacts\android\eve-debug.apk", + [string] $PackageName = "org.gamecult.eve", + [string] $ActivityName = ".MainActivity", + [int] $LaunchDelaySeconds = 3, + [int] $Width = 0, + [int] $Height = 0, + [ValidateSet("portrait", "landscape", "natural")] + [string] $Orientation = "natural", + [switch] $UseFixture, + [switch] $ForceInstall +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$absoluteOutput = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path $projectRoot $OutputPath } +$absoluteApk = if ([System.IO.Path]::IsPathRooted($ApkPath)) { $ApkPath } else { Join-Path $projectRoot $ApkPath } +New-Item -ItemType Directory -Force (Split-Path -Parent $absoluteOutput) | Out-Null + +$devices = adb devices | Select-String -Pattern "device$" +if (-not $devices) { + throw "No adb device is connected for Android parity capture." +} + +if (-not (Test-Path $absoluteApk)) { + throw "Android APK not found: $absoluteApk" +} + +$overrideSize = $Width -gt 0 -and $Height -gt 0 +$overrideRotation = $Orientation -ne "natural" +$previousWindowUserRotation = $null +$previousIgnoreOrientationRequest = $null +try { + if ($overrideRotation) { + $previousWindowUserRotation = (adb shell cmd window user-rotation).Trim() + $previousIgnoreOrientationRequest = (adb shell cmd window get-ignore-orientation-request).Trim() + $rotation = if ($Orientation -eq "landscape") { "1" } else { "0" } + adb shell cmd window set-ignore-orientation-request true | Out-Host + adb shell cmd window user-rotation lock $rotation | Out-Host + } + + if ($overrideSize -or $overrideRotation) { + adb shell am force-stop $PackageName | Out-Host + } + + if ($overrideSize) { + $wmWidth = if ($Orientation -eq "landscape") { $Height } else { $Width } + $wmHeight = if ($Orientation -eq "landscape") { $Width } else { $Height } + adb shell wm size "${wmWidth}x${wmHeight}" | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "adb wm size failed with exit code $LASTEXITCODE" + } + } + + $installed = adb shell pm path $PackageName + if ($ForceInstall -or -not $installed) { + adb install -r $absoluteApk | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "adb install failed with exit code $LASTEXITCODE" + } + } + + $launchArgs = @("shell", "am", "start", "-n", "$PackageName/$ActivityName") + if ($UseFixture) { + $launchArgs += @("--ez", "org.gamecult.eve.PARITY_FIXTURE", "true") + } + adb @launchArgs | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "adb launch failed with exit code $LASTEXITCODE" + } + + Start-Sleep -Seconds $LaunchDelaySeconds + + cmd /c "adb exec-out screencap -p > `"$absoluteOutput`"" + if ($LASTEXITCODE -ne 0) { + throw "adb screencap failed with exit code $LASTEXITCODE" + } +} finally { + if ($overrideSize) { + adb shell wm size reset | Out-Host + } + if ($overrideRotation) { + if ($previousWindowUserRotation -match "^free") { + adb shell cmd window user-rotation free | Out-Host + } elseif ($previousWindowUserRotation -match "^lock\s+(\d+)") { + adb shell cmd window user-rotation lock $Matches[1] | Out-Host + } + if ($previousIgnoreOrientationRequest -match "true") { + adb shell cmd window set-ignore-orientation-request true | Out-Host + } elseif ($previousIgnoreOrientationRequest -match "false") { + adb shell cmd window set-ignore-orientation-request false | Out-Host + } + } +} + +if (-not (Test-Path $absoluteOutput) -or (Get-Item $absoluteOutput).Length -le 0) { + throw "adb screencap did not produce a PNG: $absoluteOutput" +} + +Write-Host $absoluteOutput diff --git a/scripts/capture-flutter-parity.ps1 b/scripts/capture-flutter-parity.ps1 new file mode 100644 index 0000000..74b5739 --- /dev/null +++ b/scripts/capture-flutter-parity.ps1 @@ -0,0 +1,54 @@ +param( + [ValidateSet("windows", "linux")] + [string] $Target = "windows", + [ValidateSet("phone", "tablet", "desktop")] + [string] $ViewportId = "desktop", + [string] $FixtureId = "cultui-inspector", + [string] $OutputPath = "artifacts\parity\flutter-windows-cultui-inspector.png", + [Parameter(Mandatory = $true)] + [string] $SurfacePath, + [string] $FlutterExe = "E:\Projects\Eve\tools\deps\flutter\bin\flutter.bat" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$flutterRoot = $projectRoot +$flutter = $FlutterExe +$flutterSdkRoot = Split-Path -Parent (Split-Path -Parent $flutter) +$assetPath = Join-Path $flutterRoot "assets\current-surface.json" +$fontDir = Join-Path $flutterRoot "assets\fonts" +$absoluteOutput = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path $projectRoot $OutputPath } +New-Item -ItemType Directory -Force (Split-Path -Parent $absoluteOutput) | Out-Null +New-Item -ItemType Directory -Force (Split-Path -Parent $assetPath) | Out-Null +New-Item -ItemType Directory -Force $fontDir | Out-Null + +if (-not (Test-Path $flutter)) { + throw "Flutter is not installed at $flutter" +} + +if ($Target -eq "linux" -and $env:OS -like "Windows*") { + throw "Linux Flutter screenshot requires a Linux runner; this host is Windows." +} + +Copy-Item -LiteralPath (Join-Path $flutterSdkRoot "bin\cache\artifacts\material_fonts\roboto-regular.ttf") -Destination (Join-Path $fontDir "Roboto-Regular.ttf") -Force +Copy-Item -LiteralPath (Join-Path $flutterSdkRoot "bin\cache\artifacts\material_fonts\roboto-bold.ttf") -Destination (Join-Path $fontDir "Roboto-Bold.ttf") -Force +Copy-Item -LiteralPath $SurfacePath -Destination $assetPath -Force + +Push-Location $flutterRoot +try { + & $flutter test --update-goldens "--dart-define=EVE_PARITY_FIXTURE=$FixtureId" --plain-name $ViewportId + if ($LASTEXITCODE -ne 0) { + throw "Flutter golden smoke failed with exit code $LASTEXITCODE" + } +} finally { + Pop-Location +} + +$golden = Join-Path $flutterRoot "test\goldens\$FixtureId-$ViewportId.png" +if (-not (Test-Path $golden) -or (Get-Item $golden).Length -le 0) { + throw "Flutter golden did not produce a PNG: $golden" +} + +Copy-Item -LiteralPath $golden -Destination $absoluteOutput -Force +Write-Host $absoluteOutput diff --git a/scripts/capture-linux-flutter-parity.ps1 b/scripts/capture-linux-flutter-parity.ps1 new file mode 100644 index 0000000..f85a117 --- /dev/null +++ b/scripts/capture-linux-flutter-parity.ps1 @@ -0,0 +1,68 @@ +param( + [string] $SshTarget = "nightwing", + [ValidateSet("phone", "tablet", "desktop")] + [string] $ViewportId = "desktop", + [string] $FixtureId = "cultui-inspector", + [Parameter(Mandatory = $true)] + [string] $SurfacePath, + [string] $LocalFlutterRoot = "E:\Projects\Eve\tools\deps\flutter", + [string] $OutputPath = "artifacts\parity\linux-flutter-cultui-inspector.png", + [string] $RemoteRoot = "~/.local/share/gamecult/eve-parity-runner", + [string] $RemoteFlutter = "~/.local/share/gamecult/flutter/bin/flutter" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$sourceRoot = $projectRoot +$assetPath = Join-Path $sourceRoot "assets\current-surface.json" +$fontDir = Join-Path $sourceRoot "assets\fonts" +$absoluteOutput = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path $projectRoot $OutputPath } +$archivePath = Join-Path ([System.IO.Path]::GetTempPath()) "eve-parity-linux-$([guid]::NewGuid()).tar" +New-Item -ItemType Directory -Force (Split-Path -Parent $absoluteOutput) | Out-Null +New-Item -ItemType Directory -Force (Split-Path -Parent $assetPath) | Out-Null +New-Item -ItemType Directory -Force $fontDir | Out-Null + +if (-not (Test-Path $sourceRoot)) { + throw "Flutter parity source not found: $sourceRoot" +} + +Copy-Item -LiteralPath $SurfacePath -Destination $assetPath -Force +Copy-Item -LiteralPath (Join-Path $LocalFlutterRoot "bin\cache\artifacts\material_fonts\roboto-regular.ttf") -Destination (Join-Path $fontDir "Roboto-Regular.ttf") -Force +Copy-Item -LiteralPath (Join-Path $LocalFlutterRoot "bin\cache\artifacts\material_fonts\roboto-bold.ttf") -Destination (Join-Path $fontDir "Roboto-Bold.ttf") -Force + +try { + Push-Location $sourceRoot + try { + tar --exclude .dart_tool --exclude build -cf $archivePath . + if ($LASTEXITCODE -ne 0) { + throw "tar failed with exit code $LASTEXITCODE" + } + } finally { + Pop-Location + } + + ssh $SshTarget "rm -rf $RemoteRoot && mkdir -p $RemoteRoot" + scp $archivePath "${SshTarget}:/tmp/eve-parity-linux.tar" | Out-Host + ssh $SshTarget "tar -xf /tmp/eve-parity-linux.tar -C $RemoteRoot && rm /tmp/eve-parity-linux.tar" + ssh $SshTarget "cd $RemoteRoot && $RemoteFlutter test --update-goldens --dart-define=EVE_PARITY_FIXTURE=$FixtureId --plain-name $ViewportId" | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "Nightwing Flutter golden smoke failed with exit code $LASTEXITCODE" + } + + $remoteGolden = "$RemoteRoot/test/goldens/$FixtureId-$ViewportId.png" + scp "${SshTarget}:$remoteGolden" $absoluteOutput | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "scp from Nightwing failed with exit code $LASTEXITCODE" + } +} finally { + if (Test-Path $archivePath) { + Remove-Item -LiteralPath $archivePath -Force + } +} + +if (-not (Test-Path $absoluteOutput) -or (Get-Item $absoluteOutput).Length -le 0) { + throw "Nightwing Flutter capture did not produce a PNG: $absoluteOutput" +} + +Write-Host $absoluteOutput diff --git a/scripts/install-nightwing-flutter-sdk.ps1 b/scripts/install-nightwing-flutter-sdk.ps1 new file mode 100644 index 0000000..149198d --- /dev/null +++ b/scripts/install-nightwing-flutter-sdk.ps1 @@ -0,0 +1,17 @@ +param( + [string] $SshTarget = "nightwing" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$localScript = Join-Path $projectRoot "scripts\install-nightwing-flutter-sdk.sh" +$remoteScript = "~/.local/bin/install-eve-flutter-sdk.sh" +$stateDir = "~/.local/state/gamecult" +$pidPath = "$stateDir/eve-linux-flutter-install.pid" +$logPath = "$stateDir/eve-linux-flutter-install.log" + +ssh $SshTarget "mkdir -p ~/.local/bin ~/.local/state/gamecult" +scp $localScript "${SshTarget}:$remoteScript" | Out-Host +ssh $SshTarget "chmod +x $remoteScript" +ssh $SshTarget "nohup $remoteScript >/dev/null 2>&1 & echo `$! > $pidPath && echo $pidPath && echo $logPath" diff --git a/scripts/install-nightwing-flutter-sdk.sh b/scripts/install-nightwing-flutter-sdk.sh new file mode 100644 index 0000000..e31c119 --- /dev/null +++ b/scripts/install-nightwing-flutter-sdk.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +state_dir="$HOME/.local/state/gamecult" +sdk_dir="$HOME/.local/share/gamecult/flutter" +log_path="$state_dir/eve-linux-flutter-install.log" +status_path="$state_dir/eve-linux-flutter-install.status" + +export CI=true +export FLUTTER_SUPPRESS_ANALYTICS=true +export NO_COLOR=1 + +mkdir -p "$state_dir" "$(dirname "$sdk_dir")" + +{ + date -u '+started %Y-%m-%dT%H:%M:%SZ' + if [ ! -x "$sdk_dir/bin/flutter" ]; then + rm -rf "$sdk_dir.tmp" + git clone --depth 1 -b stable https://github.com/flutter/flutter.git "$sdk_dir.tmp" + rm -rf "$sdk_dir" + mv "$sdk_dir.tmp" "$sdk_dir" + else + git -C "$sdk_dir" fetch --depth 1 origin stable + git -C "$sdk_dir" checkout stable + git -C "$sdk_dir" pull --ff-only origin stable + fi + + "$sdk_dir/bin/flutter" --disable-analytics + "$sdk_dir/bin/flutter" --version + "$sdk_dir/bin/flutter" precache --linux + date -u '+completed %Y-%m-%dT%H:%M:%SZ' + echo complete > "$status_path" +} >> "$log_path" 2>&1 diff --git a/scripts/run-eveflutter-conformance-consumer-smoke.ps1 b/scripts/run-eveflutter-conformance-consumer-smoke.ps1 new file mode 100644 index 0000000..00c03dc --- /dev/null +++ b/scripts/run-eveflutter-conformance-consumer-smoke.ps1 @@ -0,0 +1,57 @@ +param( + [Parameter(Mandatory = $true)] + [string] $ExportDirectory, + [Parameter(Mandatory = $true)] + [string] $ConsumerScript, + [string] $ConsumerDirectory = "artifacts\conformance-consumer-smoke" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$sourceExport = if ([System.IO.Path]::IsPathRooted($ExportDirectory)) { + $ExportDirectory +} else { + Join-Path $projectRoot $ExportDirectory +} +$consumerRoot = if ([System.IO.Path]::IsPathRooted($ConsumerDirectory)) { + $ConsumerDirectory +} else { + Join-Path $projectRoot $ConsumerDirectory +} +$consumerExport = Join-Path $consumerRoot "export" + +if (-not (Test-Path $sourceExport)) { + throw "Conformance export not found: $sourceExport" +} +if (-not (Test-Path $ConsumerScript -PathType Leaf)) { + throw "Conformance consumer script not found: $ConsumerScript" +} + +if (Test-Path $consumerExport) { + Remove-Item -LiteralPath $consumerExport -Recurse -Force +} +New-Item -ItemType Directory -Force -Path $consumerRoot | Out-Null +Copy-Item -LiteralPath $sourceExport -Destination $consumerExport -Recurse + +Push-Location $projectRoot +try { + node $ConsumerScript $consumerExport ` + --expect-pack runtime ` + --expect-pack provider ` + --expect-fixture sai-vn ` + --expect-provider gamecult.home.vn ` + --expect-runtime windows-flutter ` + --expect-runtime linux-flutter ` + --expect-runtime android-flutter ` + --expect-runtime-status windows-flutter:active ` + --expect-runtime-feature windows-flutter:embeddedDocuments ` + --expect-runtime-command-schema windows-flutter:gamecult.eve.command_invocation.v1 ` + --expect-runtime-capture-status windows-flutter:golden ` + --expect-split-target EveFlutter + if ($LASTEXITCODE -ne 0) { + throw "EveFlutter conformance consumer smoke failed with exit code $LASTEXITCODE" + } +} finally { + Pop-Location +} diff --git a/scripts/run-eveflutter-lifecycle-smoke.ps1 b/scripts/run-eveflutter-lifecycle-smoke.ps1 new file mode 100644 index 0000000..d194e48 --- /dev/null +++ b/scripts/run-eveflutter-lifecycle-smoke.ps1 @@ -0,0 +1,86 @@ +param( + [string] $LifecyclePath = "eveflutter-lifecycle.json", + [string] $DartExe = "E:\Projects\Eve\tools\deps\flutter\bin\cache\dart-sdk\bin\dart.exe", + [string] $ConformanceExport = "", + [string] $ConformanceConsumerScript = "", + [switch] $SkipAnalyze +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$absoluteLifecyclePath = if ([System.IO.Path]::IsPathRooted($LifecyclePath)) { + $LifecyclePath +} else { + Join-Path $projectRoot $LifecyclePath +} + +if (-not (Test-Path $absoluteLifecyclePath)) { + throw "EveFlutter lifecycle document not found: $absoluteLifecyclePath" +} + +$lifecycle = Get-Content -LiteralPath $absoluteLifecyclePath -Raw | ConvertFrom-Json + +if ($lifecycle.schema -ne "gamecult.eve.runtime_lifecycle.v1") { + throw "Unexpected lifecycle schema: $($lifecycle.schema)" +} +if ($lifecycle.splitTarget -ne "EveFlutter") { + throw "Unexpected split target: $($lifecycle.splitTarget)" +} +if ($lifecycle.ownerRepo -ne "EveFlutter") { + throw "Unexpected owner repo: $($lifecycle.ownerRepo)" +} +if ($lifecycle.currentHostRepo -ne "EveFlutter") { + throw "Unexpected current host repo: $($lifecycle.currentHostRepo)" +} + +$expectedRuntimes = @("windows-flutter", "linux-flutter", "android-flutter") +foreach ($runtimeId in $expectedRuntimes) { + if (-not ($lifecycle.runtimes -contains $runtimeId)) { + throw "EveFlutter lifecycle missing runtime: $runtimeId" + } +} + +foreach ($stage in @("release", "test", "capture")) { + $stageDocument = $lifecycle.$stage + if ($null -eq $stageDocument) { + throw "EveFlutter lifecycle missing stage: $stage" + } + if ($stageDocument.ownerRepo -ne "EveFlutter") { + throw "EveFlutter lifecycle stage $stage has unexpected owner: $($stageDocument.ownerRepo)" + } + if (-not $stageDocument.status) { + throw "EveFlutter lifecycle stage $stage missing status" + } + foreach ($evidencePath in $stageDocument.evidencePaths) { + $absoluteEvidencePath = Join-Path $projectRoot $evidencePath + if (-not (Test-Path $absoluteEvidencePath)) { + throw "EveFlutter lifecycle stage $stage missing evidence path: $evidencePath" + } + } +} + +if (-not $SkipAnalyze) { + if (-not (Test-Path $DartExe)) { + throw "Dart SDK not found: $DartExe" + } + + Push-Location $projectRoot + try { + & $DartExe analyze lib test + if ($LASTEXITCODE -ne 0) { + throw "EveFlutter Dart analyzer failed with exit code $LASTEXITCODE" + } + } finally { + Pop-Location + } +} + +if ($ConformanceExport -and $ConformanceConsumerScript) { + & (Join-Path $projectRoot "scripts\run-eveflutter-conformance-consumer-smoke.ps1") -ExportDirectory $ConformanceExport -ConsumerScript $ConformanceConsumerScript + if ($LASTEXITCODE -ne 0) { + throw "EveFlutter conformance consumer smoke failed with exit code $LASTEXITCODE" + } +} + +Write-Host "EveFlutter lifecycle smoke passed: $absoluteLifecyclePath" diff --git a/test/goldens/cultui-inspector-desktop.png b/test/goldens/cultui-inspector-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..4db15bef273217b67334b5737c7aa63e9dddb789 GIT binary patch literal 13323 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 NE?)ar!FlIL{|Dg*{w)9i literal 0 HcmV?d00001 diff --git a/test/goldens/cultui-inspector-phone.png b/test/goldens/cultui-inspector-phone.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3455f7b0bf08b6870f2b87cd03a05f4500b57a GIT binary patch literal 10746 zcmeI2c{rO}`|o3GsZ!c%D=n(sRg@xTs%UGYQA5o|O;z(uNeN=pYO6&XQ;n&i#*$qc zN)T$JMT8n7B(=>$1Q8KKoG0(^oa=he`Qu!_^Vj+3&6VfMlU!NPy6=14Yu%sqeV$kM z&5TYQzi=D`0-b=~{lgLj;z|U8xJ{1n0KYu&KYkJTI1+XTYJCiNMIU?o68O#)W@&T> zRMRiE1Oi!dZ10(|X%oIZW>-#+O9p)8PJZhFeMe1v zi2XIOkP!#O$RpAaDk}8m@J0CNW!Awb#(w39uTzo%VLC7V=-G<+(?OGHsPLuB?MEKn zQJDX>y1?vi?IWwCJw9FQC6zYjSDj6)V}_Ab{9-73^aEW26AD=M@2^D15E(gq%hnhh z{m5j@BcrCL(ZE8By)JPM^rG^56~Pu>$xMew1wN$X4;hCSS9f{O3e>qAbR88=(c6BF zZScpslG4mSdX3dsqi9;=F~1Gm&QNk%%7I*-k|y+jUHbu}vOB}UvubwgV}b&wva+EYo#}*cc;qy&dlecJ$EEnUvgpOoBbm?=Sv-I34YR zg?0DKHGFy)bC>sc+W@J>!%xa2KkD1?5MjJNe6CnetnWrb4LC`u^Q?Bk#Cp8o$+*IX zgNhU9Rq9ug+7%Ed+aVEyWU96xs|^IcNTBU_Se~!*{;O9=7u+M}Wy1CA`%s;ixok#H zNP-jl0}qM|d~k#t6!7my*OSm0iT&O~Y4`5rUTO9UZ!JS4H%;T(|a=SS|E zy;^?CKs{BQ)Zj7FAvA}GWznHXA;hK%21SG0A!om+5c*drn|}CkTfObOf%`ooxwK*$ z+fgm$b9iooR()5i=q|fD!k?0gFSewF+Ysfe%%#4dH!k7?+(=!%Zk1hCmVl}2^pz4QJLuH~OI;tm zF>#Gs_^0j$5$~G`}A|v%COr_MAKsYllmzq0A%+ zfgJ~Tc(VD7RjJuZL!X>8%%UqrG|Mq)KZ-uY%Am!%%*#JaCrT6WP12*QVOYi4@$HWc z+PaYhtLnP_?qp}GyxWHubKUK6$sZzm2R};Kut?(bpzq)6`UBa7 zZRX)37TvWOlL|+S@Iq>zNPeq8dD@H9puO6iW_oV& z^sTeEk$rhFmdMA<8kcQ2HT@`mT1j

0q#6=VvOR@`Xf?mZDS6)q5_n16(Qttr|fS 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

pmksl94zO6Fh?Of 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 Z-%rAl9QaAbN1QKv!DI^e$Q{8 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 Ywb^-m-_LV$o95+e%9=_=e_I6o7dZ$5MF0Q* literal 0 HcmV?d00001 diff --git a/test/parity_smoke_test.dart b/test/parity_smoke_test.dart new file mode 100644 index 0000000..192b88f --- /dev/null +++ b/test/parity_smoke_test.dart @@ -0,0 +1,277 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:eve_parity/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late EveSurfaceState goldenState; + + setUpAll(() async { + final regular = FontLoader('EveParity') + ..addFont(rootBundle.load('assets/fonts/Roboto-Regular.ttf')) + ..addFont(rootBundle.load('assets/fonts/Roboto-Bold.ttf')); + await regular.load(); + goldenState = EveSurfaceState.fromJson( + jsonDecode(File('assets/current-surface.json').readAsStringSync()) + as Map, + ); + }); + + for (final viewport in _viewports) { + testWidgets('renders selected Eve parity fixture at ${viewport.id}', ( + tester, + ) async { + await tester.binding.setSurfaceSize( + Size(viewport.width, viewport.height), + ); + addTearDown(() => tester.binding.setSurfaceSize(null)); + await tester.pumpWidget( + MaterialApp(home: EveSurfaceView(state: goldenState)), + ); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + await tester.runAsync(() async { + await Future.delayed(const Duration(milliseconds: 250)); + }); + await tester.pumpAndSettle(); + expect(find.byType(EveSurfaceView), findsOneWidget); + if (fixtureId == 'cultui-inspector') { + expect(find.text('AETHERIC FIELD TESTER'), findsOneWidget); + expect(find.text('FOV'), findsOneWidget); + expect(find.text('THROTTLE DECAY'), findsOneWidget); + expect(find.text('FLOW SPEED'), findsOneWidget); + } + + await expectLater( + find.byType(EveSurfaceView), + matchesGoldenFile('goldens/$fixtureId-${viewport.id}.png'), + ); + }); + } + + test('parses embedded document slots for CultUI nested surfaces', () { + final node = EveNode.fromJson(const { + 'id': 'test.slot', + 'kind': 'surface.slot', + 'props': {'presentationKind': 'inventory.dropdown'}, + 'embeddedDocuments': [ + { + 'slotId': 'inventory.dropdown', + 'documentId': 'cultmesh://test/dropdown', + 'schemaId': 'gamecult.eve.surface.v1', + 'presentationKind': 'inventory.dropdown', + }, + ], + }); + + expect(node.embeddedDocuments, hasLength(1)); + expect(node.embeddedDocuments.single.slotId, 'inventory.dropdown'); + expect( + node.embeddedDocuments.single.documentId, + 'cultmesh://test/dropdown', + ); + }); + + test('embedded_surface_fixture_contract', () { + const json = { + 'schema': 'gamecult.eve.surface.v1', + 'surface': { + 'root': { + 'id': 'root', + 'kind': 'surface.root', + 'children': [ + { + 'id': 'slot', + 'kind': 'surface.slot', + 'embeddedDocuments': [ + { + 'slotId': 'inventory.dropdown', + 'documentId': 'cultmesh://test/dropdown', + 'schemaId': 'gamecult.eve.surface.v1', + 'presentationKind': 'inventory.dropdown', + }, + ], + }, + ], + }, + }, + }; + final surface = json['surface'] as Map; + final root = EveNode.fromJson(surface['root'] as Map); + final slot = root.children.singleWhere( + (node) => node.kind == 'surface.slot', + ); + + expect(json['schema'], 'gamecult.eve.surface.v1'); + expect(slot.embeddedDocuments, hasLength(1)); + expect(slot.embeddedDocuments.single.slotId, 'inventory.dropdown'); + expect(slot.embeddedDocuments.single.schemaId, 'gamecult.eve.surface.v1'); + expect( + slot.embeddedDocuments.single.presentationKind, + 'inventory.dropdown', + ); + }); + + testWidgets('emits a provider-routed Eve command intent', (tester) async { + final state = EveSurfaceState.fromJson(const { + 'schema': 'gamecult.eve.surface.v1', + 'providerId': 'world.example', + 'surfaceId': 'world.example.surface', + 'surface': { + 'id': 'world.example.surface', + 'root': { + 'id': 'root', + 'kind': 'surface', + 'children': [ + { + 'id': 'focus', + 'kind': 'control.button', + 'props': { + 'label': 'Focus Target', + 'command': 'world.example.commands', + 'commandId': 'world.example.focus', + 'action': {'operation': 'focus_entity'}, + }, + }, + ], + }, + }, + }); + final intents = []; + + await tester.pumpWidget( + MaterialApp( + home: EveSurfaceView( + state: state, + commandSink: (intent, node) => intents.add(intent), + ), + ), + ); + + await tester.tap(find.text('Focus Target')); + await tester.pump(); + + expect(intents, hasLength(1)); + expect(intents.single.schema, 'gamecult.eve.command_invocation.v1'); + expect(intents.single.providerId, 'world.example'); + expect(intents.single.surfaceId, 'world.example.surface'); + expect(intents.single.command, 'world.example.commands'); + expect(intents.single.commandId, 'world.example.focus'); + expect(intents.single.clientId, 'flutter-parity'); + expect(intents.single.payload['action']['operation'], 'focus_entity'); + }); + + testWidgets('provider picker consumes conformance export providers', ( + tester, + ) async { + final catalog = EveProviderCatalog.fromConformanceExport( + _providerCatalogExport, + ); + var selected = 'provider.alpha'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (context, setState) { + return EveProviderPicker( + catalog: catalog, + selectedProviderId: selected, + onSelected: (providerId) { + setState(() => selected = providerId); + }, + ); + }, + ), + ), + ), + ); + + expect(catalog.providers, hasLength(2)); + expect(catalog.findProvider('provider.beta')?.ownerRepo, 'PluginOwner'); + expect( + catalog + .findProvider('provider.beta') + ?.pluginRequirements + .map((requirement) => requirement.pluginId), + containsAll(['sai.vn', 'norn.graph', 'tex.math']), + ); + expect(find.text('provider.alpha'), findsOneWidget); + + await tester.tap(find.byType(DropdownButton)); + await tester.pumpAndSettle(); + await tester.tap(find.text('provider.beta').last); + await tester.pumpAndSettle(); + + expect(selected, 'provider.beta'); + }); +} + +const fixtureId = String.fromEnvironment( + 'EVE_PARITY_FIXTURE', + defaultValue: 'cultui-inspector', +); + +const _viewports = [ + _Viewport('phone', 390, 844), + _Viewport('tablet', 768, 1024), + _Viewport('desktop', 1280, 720), +]; + +class _Viewport { + const _Viewport(this.id, this.width, this.height); + + final String id; + final double width; + final double height; +} + +const _providerCatalogExport = { + 'schema': 'gamecult.eve.conformance_export.v1', + 'providers': [ + { + 'providerId': 'provider.alpha', + 'status': 'advertised', + 'ownerRepo': 'ProviderAlpha', + 'advertisementPath': 'provider-alpha.json', + 'surfaces': ['provider.alpha.surface'], + 'commands': ['provider.alpha.commands'], + 'pluginRequirements': [], + }, + { + 'providerId': 'provider.beta', + 'status': 'advertised', + 'ownerRepo': 'PluginOwner', + 'advertisementPath': 'provider-beta.json', + 'surfaces': ['provider.beta.surface'], + 'commands': ['provider.beta.act'], + 'pluginRequirements': [ + { + 'surfaceId': 'sai.visual_novel.surface', + 'pluginId': 'sai.vn', + 'requiredCapabilities': [ + 'vn.stage', + 'story.choose', + 'story.continue', + 'story.jump', + ], + }, + { + 'surfaceId': 'sai.visual_novel.surface', + 'pluginId': 'norn.graph', + 'requiredCapabilities': ['embed.norn'], + }, + { + 'surfaceId': 'sai.visual_novel.surface', + 'pluginId': 'tex.math', + 'requiredCapabilities': ['embed.tex'], + }, + ], + }, + ], +}; diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..e81fa7f --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(eve_parity 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 "eve_parity") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + 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() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# 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_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +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) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +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. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +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. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# 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/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app 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. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/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 + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +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} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# 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 build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..9115ddb --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "eve_parity" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "eve_parity" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "eve_parity.exe" "\0" + VALUE "ProductName", "eve_parity" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..df32e22 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"eve_parity", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3cb7146 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_