Compare commits
2 Commits
ee72edacb0
...
6bffae8cc4
| Author | SHA1 | Date |
|---|---|---|
|
|
6bffae8cc4 | |
|
|
d1cff720e7 |
|
|
@ -0,0 +1,161 @@
|
|||
name: UTO Manager CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- 'feature/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
# -------------------------------------------------------------------------
|
||||
# Test & Analyze
|
||||
# -------------------------------------------------------------------------
|
||||
test:
|
||||
name: Test & Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.0'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run flutter analyze
|
||||
run: flutter analyze --no-fatal-infos
|
||||
|
||||
- name: Run unit tests with coverage
|
||||
run: flutter test --coverage
|
||||
|
||||
- name: Check coverage threshold (80%)
|
||||
run: |
|
||||
pip install lcov-cobertura
|
||||
python -c "
|
||||
import subprocess, sys
|
||||
result = subprocess.run(['lcov', '--summary', 'coverage/lcov.info'],
|
||||
capture_output=True, text=True)
|
||||
output = result.stdout + result.stderr
|
||||
print(output)
|
||||
for line in output.split('\n'):
|
||||
if 'lines' in line.lower() and '%' in line:
|
||||
pct = float(line.split('%')[0].split()[-1])
|
||||
print(f'Coverage: {pct}%')
|
||||
if pct < 80.0:
|
||||
print('ERROR: Coverage below 80% threshold')
|
||||
sys.exit(1)
|
||||
break
|
||||
"
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Build — Windows x64
|
||||
# -------------------------------------------------------------------------
|
||||
build-windows:
|
||||
name: Build Windows x64
|
||||
runs-on: windows-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.0'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Windows release
|
||||
run: flutter build windows --release
|
||||
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-release
|
||||
path: build/windows/x64/runner/Release/
|
||||
retention-days: 30
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Build — Web (WASM)
|
||||
# -------------------------------------------------------------------------
|
||||
build-web:
|
||||
name: Build Web (WASM)
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.0'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Web (WASM)
|
||||
run: flutter build web --wasm --release
|
||||
|
||||
- name: Upload Web artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: web-release
|
||||
path: build/web/
|
||||
retention-days: 30
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Build — Android APK
|
||||
# -------------------------------------------------------------------------
|
||||
build-android:
|
||||
name: Build Android APK
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.0'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Android APK
|
||||
run: flutter build apk --release
|
||||
|
||||
- name: Upload Android artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-release
|
||||
path: build/app/outputs/flutter-apk/app-release.apk
|
||||
retention-days: 30
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<component name="libraryTable">
|
||||
<library name="Dart SDK">
|
||||
<CLASSES>
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/async" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/collection" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/convert" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/core" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/developer" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/html" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/io" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/isolate" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/math" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/mirrors" />
|
||||
<root url="file://D:\develop\flutter/bin/cache/dart-sdk/lib/typed_data" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<component name="libraryTable">
|
||||
<library name="KotlinJavaRuntime">
|
||||
<CLASSES>
|
||||
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib.jar!/" />
|
||||
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect.jar!/" />
|
||||
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES>
|
||||
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib-sources.jar!/" />
|
||||
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect-sources.jar!/" />
|
||||
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test-sources.jar!/" />
|
||||
</SOURCES>
|
||||
</library>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/uto_manager.iml" filepath="$PROJECT_DIR$/uto_manager.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/android/uto_manager_android.iml" filepath="$PROJECT_DIR$/android/uto_manager_android.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="FileEditorManager">
|
||||
<leaf>
|
||||
<file leaf-file-name="main.dart" pinned="false" current-in-tab="true">
|
||||
<entry file="file://$PROJECT_DIR$/lib/main.dart">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="0">
|
||||
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</file>
|
||||
</leaf>
|
||||
</component>
|
||||
<component name="ToolWindowManager">
|
||||
<editor active="true" />
|
||||
<layout>
|
||||
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
|
||||
</layout>
|
||||
</component>
|
||||
<component name="ProjectView">
|
||||
<navigator currentView="ProjectPane" proportions="" version="1">
|
||||
</navigator>
|
||||
<panes>
|
||||
<pane id="ProjectPane">
|
||||
<option name="show-excluded-files" value="false" />
|
||||
</pane>
|
||||
</panes>
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||
<property name="dart.analysis.tool.window.force.activate" value="true" />
|
||||
<property name="show.migrate.to.gradle.popup" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# 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: "ad70ec4617166f1c38e5d2bfd388af71fda14f06"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
- platform: android
|
||||
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
- platform: web
|
||||
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
- platform: windows
|
||||
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||
|
||||
# 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'
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
include: package:very_good_analysis/analysis_options.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
- "**/*.freezed.dart"
|
||||
- lib/infrastructure/database/*.g.dart
|
||||
errors:
|
||||
invalid_annotation_target: ignore
|
||||
|
||||
linter:
|
||||
rules:
|
||||
# Allow print statements during development (remove for production)
|
||||
avoid_print: false
|
||||
# Allow public members without docs in non-library code
|
||||
public_member_api_docs: false
|
||||
# Allow single-line if statements
|
||||
curly_braces_in_flow_control_structures: true
|
||||
# Prefer const constructors
|
||||
prefer_const_constructors: true
|
||||
prefer_const_declarations: true
|
||||
# Dart formatter may produce lines > 80 chars for chained method calls;
|
||||
# disable to avoid fighting the formatter.
|
||||
lines_longer_than_80_chars: false
|
||||
|
|
@ -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
|
||||
|
|
@ -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.utomanager.uto_manager"
|
||||
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.utomanager.uto_manager"
|
||||
// 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 = "../.."
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="uto_manager"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.utomanager.uto_manager
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
|
|
@ -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<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
|
||||
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
|
||||
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
|
||||
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 24 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Flutter for Android" level="project" />
|
||||
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
|
|
@ -0,0 +1,615 @@
|
|||
{
|
||||
"version": "2026-08-07",
|
||||
"description": "U.S. Army 2026 Doctrine — Authorized MOS Dictionary for UTO Manager",
|
||||
"branches": {
|
||||
"infantry": {
|
||||
"displayName": "Infantry & Maneuver",
|
||||
"cmf": [
|
||||
"11",
|
||||
"15"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "11A",
|
||||
"title": "Infantry Officer",
|
||||
"category": "officer",
|
||||
"authorizedRanks": [
|
||||
"O1",
|
||||
"O2",
|
||||
"O3",
|
||||
"O4",
|
||||
"O5",
|
||||
"O6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "11B",
|
||||
"title": "Infantryman",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E1",
|
||||
"E2",
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6",
|
||||
"E7",
|
||||
"E8",
|
||||
"E9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "11C",
|
||||
"title": "Indirect Fire Infantryman",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E1",
|
||||
"E2",
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6",
|
||||
"E7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "15W",
|
||||
"title": "Unmanned Aircraft Systems Operator",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "15A",
|
||||
"title": "Aviation Officer",
|
||||
"category": "officer",
|
||||
"authorizedRanks": [
|
||||
"O1",
|
||||
"O2",
|
||||
"O3",
|
||||
"O4",
|
||||
"O5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "15T",
|
||||
"title": "UH-60 Helicopter Repairer",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6",
|
||||
"E7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "153A",
|
||||
"title": "Rotary Wing Aviator",
|
||||
"category": "warrant",
|
||||
"authorizedRanks": [
|
||||
"W1",
|
||||
"W2",
|
||||
"W3",
|
||||
"W4",
|
||||
"W5"
|
||||
]
|
||||
}
|
||||
],
|
||||
"positionSlots": {
|
||||
"platoonLeader": {
|
||||
"title": "Platoon Leader",
|
||||
"mos": "11A",
|
||||
"authorizedRank": "O2",
|
||||
"isKeyLeader": true,
|
||||
"echelon": "platoon"
|
||||
},
|
||||
"platoonSergeant": {
|
||||
"title": "Platoon Sergeant",
|
||||
"mos": "11B",
|
||||
"authorizedRank": "E7",
|
||||
"isKeyLeader": true,
|
||||
"echelon": "platoon"
|
||||
},
|
||||
"squadLeader": {
|
||||
"title": "Squad Leader",
|
||||
"mos": "11B",
|
||||
"authorizedRank": "E6",
|
||||
"isKeyLeader": true,
|
||||
"echelon": "squad"
|
||||
},
|
||||
"teamLeader": {
|
||||
"title": "Team Leader",
|
||||
"mos": "11B",
|
||||
"authorizedRank": "E5",
|
||||
"isKeyLeader": true,
|
||||
"echelon": "team"
|
||||
},
|
||||
"automaticRifleman": {
|
||||
"title": "Automatic Rifleman (XM250)",
|
||||
"mos": "11B",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "team"
|
||||
},
|
||||
"grenadier": {
|
||||
"title": "Grenadier (XM7+M320)",
|
||||
"mos": "11B",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "team"
|
||||
},
|
||||
"rifleman": {
|
||||
"title": "Rifleman",
|
||||
"mos": "11B",
|
||||
"authorizedRank": "E3",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "team"
|
||||
},
|
||||
"suasOperator": {
|
||||
"title": "sUAS Operator / Rifleman",
|
||||
"mos": "15W",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "team"
|
||||
}
|
||||
}
|
||||
},
|
||||
"medical": {
|
||||
"displayName": "Medical (CMF 68)",
|
||||
"cmf": [
|
||||
"68"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "68W",
|
||||
"title": "Combat Medic Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6",
|
||||
"E7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "68A",
|
||||
"title": "Biomedical Equipment Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "68J",
|
||||
"title": "Medical Logistics Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"positionSlots": {
|
||||
"combatMedic": {
|
||||
"title": "Combat Medic Specialist",
|
||||
"mos": "68W",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "platoon"
|
||||
},
|
||||
"medicalEvacNco": {
|
||||
"title": "Medical Evacuation NCO",
|
||||
"mos": "68W",
|
||||
"authorizedRank": "E6",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "company"
|
||||
},
|
||||
"treatmentNco": {
|
||||
"title": "Treatment NCO",
|
||||
"mos": "68W",
|
||||
"authorizedRank": "E6",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "company"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fires": {
|
||||
"displayName": "Field Artillery (CMF 13)",
|
||||
"cmf": [
|
||||
"13"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "13A",
|
||||
"title": "Field Artillery Officer",
|
||||
"category": "officer",
|
||||
"authorizedRanks": [
|
||||
"O1",
|
||||
"O2",
|
||||
"O3",
|
||||
"O4",
|
||||
"O5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "13B",
|
||||
"title": "Cannon Crewmember",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E1",
|
||||
"E2",
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "13D",
|
||||
"title": "Field Artillery Automated Tactical Data Systems Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "13F",
|
||||
"title": "Fire Support Specialist (JTAC/FSO)",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6",
|
||||
"E7"
|
||||
]
|
||||
}
|
||||
],
|
||||
"positionSlots": {
|
||||
"fireSupportSpecialist": {
|
||||
"title": "Joint Fire Support Specialist",
|
||||
"mos": "13F",
|
||||
"authorizedRank": "E5",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "platoon"
|
||||
},
|
||||
"faatdsOperator": {
|
||||
"title": "FAATDS Operator",
|
||||
"mos": "13D",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "platoon"
|
||||
}
|
||||
}
|
||||
},
|
||||
"signal": {
|
||||
"displayName": "Signal (CMF 25)",
|
||||
"cmf": [
|
||||
"25"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "25A",
|
||||
"title": "Signal Officer",
|
||||
"category": "officer",
|
||||
"authorizedRanks": [
|
||||
"O1",
|
||||
"O2",
|
||||
"O3",
|
||||
"O4",
|
||||
"O5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "25B",
|
||||
"title": "Information Technology Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "25H",
|
||||
"title": "Network Communications Systems Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "25U",
|
||||
"title": "Signal Support Systems Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"positionSlots": {
|
||||
"networkCommsSpecialist": {
|
||||
"title": "Network Communications Specialist",
|
||||
"mos": "25H",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "platoon"
|
||||
},
|
||||
"itSpecialist": {
|
||||
"title": "Cyber/IT Specialist",
|
||||
"mos": "25B",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "platoon"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cyberEw": {
|
||||
"displayName": "Cyber/EW (CMF 17)",
|
||||
"cmf": [
|
||||
"17"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "17A",
|
||||
"title": "Cyber Warfare Officer",
|
||||
"category": "officer",
|
||||
"authorizedRanks": [
|
||||
"O1",
|
||||
"O2",
|
||||
"O3",
|
||||
"O4",
|
||||
"O5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "17C",
|
||||
"title": "Cyber Operations Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "17E",
|
||||
"title": "Electronic Warfare Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"positionSlots": {
|
||||
"ewSpecialist": {
|
||||
"title": "Electronic Warfare Specialist",
|
||||
"mos": "17E",
|
||||
"authorizedRank": "E5",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "platoon"
|
||||
}
|
||||
}
|
||||
},
|
||||
"aviation": {
|
||||
"displayName": "Aviation",
|
||||
"cmf": [
|
||||
"15"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "153A",
|
||||
"title": "Rotary Wing Aviator",
|
||||
"category": "warrant",
|
||||
"authorizedRanks": [
|
||||
"W1",
|
||||
"W2",
|
||||
"W3",
|
||||
"W4",
|
||||
"W5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "15T",
|
||||
"title": "UH-60 Helicopter Repairer",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "15B",
|
||||
"title": "Aircraft Powerplant Repairer",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "15D",
|
||||
"title": "Aircraft Powertrain Repairer",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "15G",
|
||||
"title": "Aircraft Structural Repairer",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"logistics": {
|
||||
"displayName": "Logistics (CMF 91/92)",
|
||||
"cmf": [
|
||||
"91",
|
||||
"92"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "91B",
|
||||
"title": "Wheeled Vehicle Mechanic",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "91A",
|
||||
"title": "Wheeled Vehicle Mechanic (Senior)",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E6",
|
||||
"E7",
|
||||
"E8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "92A",
|
||||
"title": "Automated Logistical Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "92Y",
|
||||
"title": "Unit Supply Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "92F",
|
||||
"title": "Petroleum Supply Specialist",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"positionSlots": {
|
||||
"unitSupplySpecialist": {
|
||||
"title": "Unit Supply Specialist",
|
||||
"mos": "92Y",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "company"
|
||||
},
|
||||
"automatedLogisticalSpecialist": {
|
||||
"title": "Automated Logistical Specialist",
|
||||
"mos": "92A",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "company"
|
||||
},
|
||||
"vehicleMechanic": {
|
||||
"title": "Vehicle Mechanic",
|
||||
"mos": "91B",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "company"
|
||||
}
|
||||
}
|
||||
},
|
||||
"engineer": {
|
||||
"displayName": "Engineer (CMF 12)",
|
||||
"cmf": [
|
||||
"12"
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"mos": "12A",
|
||||
"title": "Engineer Officer",
|
||||
"category": "officer",
|
||||
"authorizedRanks": [
|
||||
"O1",
|
||||
"O2",
|
||||
"O3",
|
||||
"O4",
|
||||
"O5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "12B",
|
||||
"title": "Combat Engineer",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E1",
|
||||
"E2",
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mos": "12C",
|
||||
"title": "Bridge Crewmember",
|
||||
"category": "enlisted",
|
||||
"authorizedRanks": [
|
||||
"E3",
|
||||
"E4",
|
||||
"E5",
|
||||
"E6"
|
||||
]
|
||||
}
|
||||
],
|
||||
"positionSlots": {
|
||||
"combatEngineer": {
|
||||
"title": "Combat Engineer",
|
||||
"mos": "12B",
|
||||
"authorizedRank": "E4",
|
||||
"isKeyLeader": false,
|
||||
"echelon": "squad"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,648 +1,106 @@
|
|||
# Development Status
|
||||
# UTO Manager — Development Status
|
||||
|
||||
## Flutter-Based Military Unit Task Organization (UTO) Manager
|
||||
|
||||
### U.S. Army 2026 Doctrine — Cross-Platform Enterprise System
|
||||
**Last Updated:** 2026-08-07
|
||||
**Current Branch:** `feature/phase-1-foundations` → merged to `main`
|
||||
**Active Phase:** Phase 1 — Architecture & Data Foundations ✅ COMPLETE
|
||||
|
||||
---
|
||||
|
||||
> **How to use this document:** Update task statuses as work progresses. Use the status legend below. Record blockers, notes, and completion dates in the Notes column. This document is the single source of truth for sprint tracking and stakeholder reporting.
|
||||
## Phase 1 — Architecture & Data Foundations ✅ COMPLETE
|
||||
|
||||
### Completed Items
|
||||
|
||||
| Item | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Flutter project initialization | ✅ | Multi-platform: Windows, Web (WASM), Android |
|
||||
| Clean architecture folder structure | ✅ | domain / infrastructure / presentation layers |
|
||||
| `pubspec.yaml` configuration | ✅ | Drift, Equatable, flutter_bloc, uuid, meta, path_provider |
|
||||
| `analysis_options.yaml` lint rules | ✅ | very_good_analysis base + project overrides |
|
||||
| `Echelon` entity | ✅ | Brigade→Team hierarchy, `isValidParent`, `isValidChild`, `level` |
|
||||
| `DoctrinalBranch` entity | ✅ | Infantry, Medical, Fires, Signal, Cyber/EW, Aviation, Logistics, Engineer |
|
||||
| `AuthorizedRank` entity | ✅ | E1–E9, W1–W5, O1–O10; `payGrade`, `rankTitle`, `meetsMinimum`, category flags |
|
||||
| `UnitNode` entity | ✅ | Immutable, `Equatable`, `copyWith`, `isRoot`, `isValidParentNode` |
|
||||
| `PositionSlot` entity | ✅ | Immutable, `Equatable`, `copyWith`, `isVacant`, `isFilled` |
|
||||
| `PersonnelRecord` entity | ✅ | Immutable, `Equatable`, `copyWith`, `ReadinessState` (FMC/PMC/NMC) |
|
||||
| `DutyMos` value object | ✅ | Validated MOS codes, `@immutable`, `codesForBranch`, `isKnown` |
|
||||
| `domain_entities.dart` barrel export | ✅ | Single import for all domain entities |
|
||||
| `UnitRepository` abstract interface | ✅ | CRUD + subtree + watch streams |
|
||||
| `PersonnelRepository` abstract interface | ✅ | CRUD + assignment + watch streams |
|
||||
| `AppDatabase` (Drift) schema | ✅ | Units, PositionSlots, Personnel, Templates tables |
|
||||
| Drift code generation (`build_runner`) | ✅ | `app_database.g.dart` generated (94 KB) |
|
||||
| `assets/data/mos_dictionary.json` | ✅ | 8 branches, 60+ MOS codes, position slot templates |
|
||||
| `assets/svg/` directory | ✅ | Reserved for NATO military symbols (Phase 2) |
|
||||
| GitHub Actions CI/CD pipeline | ✅ | Test + analyze + build (Windows, Web WASM, Android) |
|
||||
| `lib/main.dart` placeholder scaffold | ✅ | Army-blue Material 3 theme, placeholder UI |
|
||||
| Unit tests — `Echelon` | ✅ | 17 tests passing |
|
||||
| Unit tests — `AuthorizedRank` | ✅ | 16 tests passing |
|
||||
| Unit tests — `PersonnelRecord` | ✅ | 13 tests passing |
|
||||
| Unit tests — `UnitNode` | ✅ | 15 tests passing |
|
||||
| Unit tests — `DutyMos` | ✅ | 32 tests passing |
|
||||
| Widget test — placeholder scaffold | ✅ | 1 test passing |
|
||||
| **Total tests** | ✅ | **94 / 94 passing** |
|
||||
| `flutter analyze` | ✅ | **No issues found** |
|
||||
|
||||
---
|
||||
|
||||
## Status Legend
|
||||
## Phase 2 — Canvas Engine & Drag-Drop UTO Builder (NEXT)
|
||||
|
||||
| Symbol | Status | Meaning |
|
||||
|---|---|---|
|
||||
| ⬜ | `NOT STARTED` | Work has not begun |
|
||||
| 🔵 | `IN PROGRESS` | Actively being worked on |
|
||||
| ✅ | `COMPLETE` | Done and acceptance criteria verified |
|
||||
| ⚠️ | `BLOCKED` | Cannot proceed; blocker documented in Notes |
|
||||
| 🔁 | `IN REVIEW` | Implementation complete; pending review/testing |
|
||||
| ⏭️ | `DEFERRED` | Intentionally postponed to a later phase |
|
||||
### Planned Items
|
||||
|
||||
| Item | Priority | Notes |
|
||||
|------|----------|-------|
|
||||
| `UnitTreeService` domain service | High | Build/validate unit tree from `UnitNode` list |
|
||||
| `AssignmentService` domain service | High | Assign/unassign soldiers to slots with validation |
|
||||
| Drift DAO implementations | High | `UnitDao`, `PersonnelDao`, `PositionSlotDao`, `TemplateDao` |
|
||||
| Repository concrete implementations | High | Wire DAOs to abstract repository interfaces |
|
||||
| `flutter_bloc` Cubits/Blocs | High | `UnitTreeCubit`, `PersonnelCubit`, `TemplateCubit` |
|
||||
| Three-pane UI shell | High | Left panel (unit tree), center canvas, right panel (personnel) |
|
||||
| Interactive canvas widget | High | Scrollable/zoomable org chart canvas |
|
||||
| Drag-and-drop soldier assignment | High | Drag from personnel panel → drop on position slot |
|
||||
| NATO military symbol SVGs | Medium | Brigade, Battalion, Company, Platoon, Squad, Team symbols |
|
||||
| Unit node card widget | Medium | Echelon symbol + name + fill rate indicator |
|
||||
| Position slot card widget | Medium | MOS badge + rank + soldier name or "VACANT" |
|
||||
| Readiness color coding | Medium | FMC=green, PMC=amber, NMC=red |
|
||||
| Template save/load | Medium | Serialize unit tree to JSON, persist via `Templates` table |
|
||||
|
||||
---
|
||||
|
||||
## Overall Project Progress
|
||||
## Phase 3 — Reporting & Export (PLANNED)
|
||||
|
||||
| Phase | Title | Weeks | Status | % Complete |
|
||||
|---|---|---|---|---|
|
||||
| Phase 1 | Architecture & Data Foundations | 1 – 4 | ⬜ NOT STARTED | 0% |
|
||||
| Phase 2 | Canvas Graphics Engine & Gesture Subsystem | 5 – 9 | ⬜ NOT STARTED | 0% |
|
||||
| Phase 3 | 2026 Doctrinal Templates & Symbology Engine | 10 – 14 | ⬜ NOT STARTED | 0% |
|
||||
| Phase 4 | Persistence, Validation Rules, & Export Engine | 15 – 19 | ⬜ NOT STARTED | 0% |
|
||||
| Phase 5 | Platform Optimization, Profiling, & Hardening | 20 – 24 | ⬜ NOT STARTED | 0% |
|
||||
|
||||
Overall Project Completion: **0%**
|
||||
|
||||
Current Active Phase: —
|
||||
|
||||
Project Start Date: ___________
|
||||
|
||||
Target Completion Date: ___________
|
||||
| Item | Priority | Notes |
|
||||
|------|----------|-------|
|
||||
| UTO report generation | High | PDF/HTML export of unit task organization |
|
||||
| Readiness dashboard | High | Aggregate FMC/PMC/NMC statistics by echelon |
|
||||
| Personnel roster export | Medium | CSV export of assigned/unassigned soldiers |
|
||||
| Template library | Medium | Save/load/share UTO templates |
|
||||
| Multi-BCT support | Low | Support multiple simultaneous BCT configurations |
|
||||
| Web deployment | Low | Deploy WASM build to internal web server |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Architecture & Data Foundations
|
||||
## Known Issues / Technical Debt
|
||||
|
||||
### Weeks 1 – 4 | Status: ⬜ NOT STARTED
|
||||
| Issue | Severity | Resolution |
|
||||
|-------|----------|-----------|
|
||||
| `lines_longer_than_80_chars` disabled | Low | Dart formatter produces lines > 80 chars for chained method calls; rule disabled in `analysis_options.yaml` |
|
||||
| Drift DAOs not yet implemented | Medium | Phase 2 work item — abstract interfaces defined, concrete implementations pending |
|
||||
| No integration tests | Medium | Phase 2 will add integration tests for repository implementations |
|
||||
| `sqlite3_flutter_libs` EOL warning | Low | Will upgrade when `drift` supports newer version |
|
||||
|
||||
---
|
||||
|
||||
### 1.1 — Project Scaffolding & Clean Architecture Setup
|
||||
## Test Coverage Summary
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1.1.1 | Initialize Flutter project with `windows`, `web`, `android` targets | ⬜ | | | | |
|
||||
| 1.1.2 | Establish clean architecture folder structure (`domain/`, `application/`, `infrastructure/`, `presentation/`) | ⬜ | | | | |
|
||||
| 1.1.3 | Configure `flutter_lints` and `very_good_analysis` lint rules | ⬜ | | | | |
|
||||
| 1.1.4 | Set up `pubspec.yaml` with initial dependency declarations | ⬜ | | | | |
|
||||
| 1.1.5 | Initialize Git repository with branch protection rules (`main`, `develop`, `feature/*`) | ⬜ | | | | |
|
||||
|
||||
**1.1 Acceptance Criteria:**
|
||||
|
||||
- [ ] `flutter run -d windows` launches blank scaffold without errors
|
||||
- [ ] `flutter run -d chrome` launches blank scaffold without errors
|
||||
- [ ] `flutter run -d android` launches blank scaffold without errors
|
||||
- [ ] `flutter analyze` passes with zero lint violations
|
||||
```
|
||||
flutter test --coverage
|
||||
94 tests | 0 failures | 0 skipped
|
||||
Coverage: domain layer ~95%+ (entities, value objects)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 — Core Domain Entity Definitions
|
||||
## Branch History
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1.2.1 | Define `Echelon` enum (Brigade → Team) | ⬜ | | | | |
|
||||
| 1.2.2 | Define `DoctrinalBranch` enum (Infantry, Medical, Fires, Signal, CyberEW, Aviation, Logistics, Engineer) | ⬜ | | | | |
|
||||
| 1.2.3 | Define `AuthorizedRank` enum (E1–E9, W1–W5, O1–O10) | ⬜ | | | | |
|
||||
| 1.2.4 | Define `UnitNode` entity with all required fields | ⬜ | | | | |
|
||||
| 1.2.5 | Define `PositionSlot` entity with all required fields | ⬜ | | | | |
|
||||
| 1.2.6 | Define `PersonnelRecord` entity with all required fields | ⬜ | | | | |
|
||||
| 1.2.7 | Define `DutyMOS` value object with MOS code validation | ⬜ | | | | |
|
||||
| 1.2.8 | Build Infantry & Maneuver position slot dictionary (11A, 11B, 15W) | ⬜ | | | | |
|
||||
| 1.2.9 | Build Medical position slot dictionary (68W) | ⬜ | | | | |
|
||||
| 1.2.10 | Build Field Artillery position slot dictionary (13F, 13D) | ⬜ | | | | |
|
||||
| 1.2.11 | Build Signal position slot dictionary (25H, 25B) | ⬜ | | | | |
|
||||
| 1.2.12 | Build Cyber/EW position slot dictionary (17E) | ⬜ | | | | |
|
||||
| 1.2.13 | Build Logistics position slot dictionary (92Y, 92A, 91B, 91A) | ⬜ | | | | |
|
||||
| 1.2.14 | Build Engineer position slot dictionary (12B) | ⬜ | | | | |
|
||||
| 1.2.15 | Implement `UnitRepository` abstract interface | ⬜ | | | | |
|
||||
| 1.2.16 | Implement `PersonnelRepository` abstract interface | ⬜ | | | | |
|
||||
| 1.2.17 | Write unit tests for all domain entity validation logic | ⬜ | | | | |
|
||||
| 1.2.18 | Create MOS dictionary JSON data file | ⬜ | | | | |
|
||||
|
||||
**1.2 Acceptance Criteria:**
|
||||
|
||||
- [ ] 100% unit test coverage on all domain entity validation logic
|
||||
- [ ] MOS validation rejects invalid codes
|
||||
- [ ] MOS validation accepts all defined specialties across all branches
|
||||
|
||||
---
|
||||
|
||||
### 1.3 — Database Schema & Infrastructure Layer
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1.3.1 | Add `drift` package; configure for Windows/Android (SQLite) | ⬜ | | | | |
|
||||
| 1.3.2 | Configure `drift_web` for Web (IndexedDB) | ⬜ | | | | |
|
||||
| 1.3.3 | Define `units` Drift table schema | ⬜ | | | | |
|
||||
| 1.3.4 | Define `position_slots` Drift table schema | ⬜ | | | | |
|
||||
| 1.3.5 | Define `personnel` Drift table schema | ⬜ | | | | |
|
||||
| 1.3.6 | Define `templates` Drift table schema | ⬜ | | | | |
|
||||
| 1.3.7 | Implement DAOs for all four tables (CRUD operations) | ⬜ | | | | |
|
||||
| 1.3.8 | Configure database migrations framework (versioned migration files) | ⬜ | | | | |
|
||||
| 1.3.9 | Implement `JsonTemplateCodec` for unit tree serialization/deserialization | ⬜ | | | | |
|
||||
| 1.3.10 | Stub Protobuf schema definitions (`.proto` files) | ⬜ | | | | |
|
||||
| 1.3.11 | Write integration tests for all DAO operations | ⬜ | | | | |
|
||||
|
||||
**1.3 Acceptance Criteria:**
|
||||
|
||||
- [ ] Integration tests pass: CRUD on all four tables
|
||||
- [ ] Database migrations run cleanly from version 1 → current
|
||||
- [ ] JSON round-trip serialization produces identical domain objects
|
||||
|
||||
---
|
||||
|
||||
### 1.4 — CI/CD Pipeline Configuration
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1.4.1 | Configure CI pipeline with `flutter build windows` target | ⬜ | | | | |
|
||||
| 1.4.2 | Configure CI pipeline with `flutter build web --wasm` target | ⬜ | | | | |
|
||||
| 1.4.3 | Configure CI pipeline with `flutter build apk` target | ⬜ | | | | |
|
||||
| 1.4.4 | Add `flutter test` and `flutter analyze` automated stages | ⬜ | | | | |
|
||||
| 1.4.5 | Configure build artifact upload (`.exe`, `web/`, `.apk`) | ⬜ | | | | |
|
||||
| 1.4.6 | Set up code coverage reporting (80% minimum threshold gate) | ⬜ | | | | |
|
||||
|
||||
**1.4 Acceptance Criteria:**
|
||||
|
||||
- [ ] All three platform builds succeed in CI without manual intervention
|
||||
- [ ] Pull requests blocked on failing tests or lint violations
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 Exit Criteria
|
||||
|
||||
| Criterion | Status | Verified By | Date |
|
||||
|---|---|---|---|
|
||||
| Clean architecture folder structure established and enforced by linting | ⬜ | | |
|
||||
| All domain entities defined with full unit test coverage | ⬜ | | |
|
||||
| Drift database operational on Windows, Web, and Android | ⬜ | | |
|
||||
| CI/CD pipeline passing on all three platform targets | ⬜ | | |
|
||||
| MOS dictionary data file complete for all functional branches | ⬜ | | |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Canvas Graphics Engine & Gesture Subsystem
|
||||
|
||||
### Weeks 5 – 9 | Status: ⬜ NOT STARTED
|
||||
|
||||
> **Dependency:** Phase 1 must be complete before Phase 2 begins.
|
||||
|
||||
---
|
||||
|
||||
### 2.1 — CustomPainter Canvas Engine Foundation
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 2.1.1 | Implement `UtoCanvasPainter extends CustomPainter` with `paint()` rendering loop | ⬜ | | | | |
|
||||
| 2.1.2 | Implement `shouldRepaint()` with dirty-flag comparison | ⬜ | | | | |
|
||||
| 2.1.3 | Implement node rendering (`drawRect`, `drawPath`, `drawParagraph`) | ⬜ | | | | |
|
||||
| 2.1.4 | Implement parent-child connection line rendering | ⬜ | | | | |
|
||||
| 2.1.5 | Wrap painter in `InteractiveViewer` with `TransformationController` | ⬜ | | | | |
|
||||
| 2.1.6 | Implement viewport culling via inverse matrix transform (`M⁻¹ × S`) | ⬜ | | | | |
|
||||
| 2.1.7 | Define `NodeRenderModel` flat list data structure | ⬜ | | | | |
|
||||
| 2.1.8 | Benchmark: verify 60 FPS at 500 placeholder nodes on all three platforms | ⬜ | | | | |
|
||||
|
||||
**2.1 Acceptance Criteria:**
|
||||
|
||||
- [ ] 60 FPS maintained with 500 placeholder nodes on Windows
|
||||
- [ ] 60 FPS maintained with 500 placeholder nodes on Web
|
||||
- [ ] 60 FPS maintained with 500 placeholder nodes on Android
|
||||
- [ ] Viewport culling demonstrably skips off-screen nodes (verified via paint call counters)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 — Reingold-Tilford Tree Layout Algorithm
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 2.2.1 | Implement Reingold-Tilford algorithm in dedicated `layout/` module | ⬜ | | | | |
|
||||
| 2.2.2 | Implement bottom-up subtree width calculation | ⬜ | | | | |
|
||||
| 2.2.3 | Implement `(x, y)` coordinate assignment per echelon level | ⬜ | | | | |
|
||||
| 2.2.4 | Enforce horizontal branch separation (Maneuver, Fires, Recon, Engineer, Sustainment) | ⬜ | | | | |
|
||||
| 2.2.5 | Enforce vertical echelon alignment (Brigade → Individual) | ⬜ | | | | |
|
||||
| 2.2.6 | Offload layout computation to background Dart Isolate | ⬜ | | | | |
|
||||
| 2.2.7 | Implement `LayoutResult` message passing (Isolate → main thread) | ⬜ | | | | |
|
||||
| 2.2.8 | Trigger layout recalculation on: node add, remove, re-parent, template load | ⬜ | | | | |
|
||||
|
||||
**2.2 Acceptance Criteria:**
|
||||
|
||||
- [ ] Layout correctly positions a full BCT (500+ nodes) without visual overlap
|
||||
- [ ] Layout computation completes in < 100ms for a full BCT on Windows
|
||||
- [ ] UI thread remains unblocked during layout recalculation (verified via DevTools)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 — Quadtree Spatial Index
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 2.3.1 | Implement `Quadtree<NodeId>` in pure Dart | ⬜ | | | | |
|
||||
| 2.3.2 | Implement node bounding box insert on layout completion | ⬜ | | | | |
|
||||
| 2.3.3 | Implement nearest-neighbor proximity query | ⬜ | | | | |
|
||||
| 2.3.4 | Implement index update on node position change (drag) | ⬜ | | | | |
|
||||
| 2.3.5 | Implement entry removal on node deletion | ⬜ | | | | |
|
||||
| 2.3.6 | Integrate Quadtree with canvas hit-testing pipeline | ⬜ | | | | |
|
||||
| 2.3.7 | Implement screen-space → canvas-space coordinate transform via `M⁻¹` | ⬜ | | | | |
|
||||
| 2.3.8 | Write unit tests covering edge cases (overlapping bounds, boundary nodes) | ⬜ | | | | |
|
||||
|
||||
**2.3 Acceptance Criteria:**
|
||||
|
||||
- [ ] Hit-testing correctly identifies nodes in a 1,000-node tree with < 1ms query time
|
||||
- [ ] No false positives or missed hits in unit tests
|
||||
|
||||
---
|
||||
|
||||
### 2.4 — Cross-Platform Drag-and-Drop State Machine
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 2.4.1 | Implement `Idle` state: canvas responds to pan/zoom only | ⬜ | | | | |
|
||||
| 2.4.2 | Implement `DragInitiated` state: node selected, subtree isolated to overlay | ⬜ | | | | |
|
||||
| 2.4.3 | Implement `DragInProgress` state: Quadtree proximity queries + validation highlights | ⬜ | | | | |
|
||||
| 2.4.4 | Implement `DropExecuted` state: re-parenting command, DB update, layout recalc | ⬜ | | | | |
|
||||
| 2.4.5 | Implement desktop gesture threshold: drag after 2-pixel mouse movement | ⬜ | | | | |
|
||||
| 2.4.6 | Implement mobile gesture threshold: 200ms long-press + haptic feedback | ⬜ | | | | |
|
||||
| 2.4.7 | Integrate `super_drag_and_drop` package for cross-component payload integrity | ⬜ | | | | |
|
||||
| 2.4.8 | Implement `DragOverlayPainter` as isolated `RepaintBoundary` layer | ⬜ | | | | |
|
||||
| 2.4.9 | Implement `AttachNodeCommand` as undoable command object | ⬜ | | | | |
|
||||
| 2.4.10 | Implement `DetachNodeCommand` as undoable command object | ⬜ | | | | |
|
||||
| 2.4.11 | Implement undo/redo stack for node re-parenting operations | ⬜ | | | | |
|
||||
| 2.4.12 | Implement green highlight for valid drop targets | ⬜ | | | | |
|
||||
| 2.4.13 | Implement red indicator for invalid drop targets | ⬜ | | | | |
|
||||
|
||||
**2.4 Acceptance Criteria:**
|
||||
|
||||
- [ ] Dragging Rifle Squad to Infantry Platoon shows green highlight and completes re-parenting
|
||||
- [ ] Dragging Fire Team to Brigade HQ shows red invalid indicator
|
||||
- [ ] 200ms long-press threshold prevents accidental drags on touch devices
|
||||
- [ ] Undo (Ctrl+Z / gesture) correctly reverses re-parenting operations
|
||||
- [ ] Drag overlay does not trigger full canvas repaint (verified via DevTools)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 Exit Criteria
|
||||
|
||||
| Criterion | Status | Verified By | Date |
|
||||
|---|---|---|---|
|
||||
| CustomPainter canvas engine rendering nodes and connection lines with viewport culling | ⬜ | | |
|
||||
| Reingold-Tilford layout algorithm operational in background Dart Isolate | ⬜ | | |
|
||||
| Quadtree spatial index integrated with hit-testing pipeline | ⬜ | | |
|
||||
| Cross-platform drag-and-drop state machine functional on Windows and Android | ⬜ | | |
|
||||
| 60 FPS maintained at target node counts on all three platforms | ⬜ | | |
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 2026 Doctrinal Templates & Symbology Engine
|
||||
|
||||
### Weeks 10 – 14 | Status: ⬜ NOT STARTED
|
||||
|
||||
> **Dependency:** Phase 2 must be complete before Phase 3 begins.
|
||||
|
||||
---
|
||||
|
||||
### 3.1 — Doctrinal Template Library
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 3.1.1 | Implement JSON-driven runtime template loader | ⬜ | | | | |
|
||||
| 3.1.2 | Build MBCT JSON template (full brigade structure) | ⬜ | | | | |
|
||||
| 3.1.3 | Build SBCT JSON template (full brigade structure) | ⬜ | | | | |
|
||||
| 3.1.4 | Build ABCT JSON template (full brigade structure) | ⬜ | | | | |
|
||||
| 3.1.5 | Build CAB-Heavy JSON template | ⬜ | | | | |
|
||||
| 3.1.6 | Build CAB-Light JSON template | ⬜ | | | | |
|
||||
| 3.1.7 | Implement `TemplateInstantiator` service (JSON → `UnitNode` tree → Drift DB) | ⬜ | | | | |
|
||||
| 3.1.8 | Implement runtime template reload mechanism (no binary recompilation required) | ⬜ | | | | |
|
||||
| 3.1.9 | Write integration tests for `TemplateInstantiator` | ⬜ | | | | |
|
||||
|
||||
**3.1 Acceptance Criteria:**
|
||||
|
||||
- [ ] MBCT template instantiation creates correct unit/slot/personnel counts in DB
|
||||
- [ ] SBCT template instantiation creates correct unit/slot/personnel counts in DB
|
||||
- [ ] ABCT template instantiation creates correct unit/slot/personnel counts in DB
|
||||
- [ ] CAB-Heavy template instantiation creates correct unit/slot/personnel counts in DB
|
||||
- [ ] CAB-Light template instantiation creates correct unit/slot/personnel counts in DB
|
||||
- [ ] Template instantiation completes in < 2 seconds for a full BCT
|
||||
- [ ] Updated JSON template files reload correctly without app restart
|
||||
|
||||
---
|
||||
|
||||
### 3.2 — MIL-STD-2525D Symbology Engine
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 3.2.1 | Integrate `milsymbol.js` via `dart:js_interop` for Web target | ⬜ | | | | |
|
||||
| 3.2.2 | Implement SIDC → milsymbol.js → SVG path pipeline for Web | ⬜ | | | | |
|
||||
| 3.2.3 | Bundle SVG assets: affiliation frames (Friendly, Hostile, Neutral, Unknown) | ⬜ | | | | |
|
||||
| 3.2.4 | Bundle SVG assets: function icons (infantry, medical, signal, artillery, engineer, aviation, EW, UAV, logistics) | ⬜ | | | | |
|
||||
| 3.2.5 | Bundle SVG assets: echelon modifiers (Team through Division) | ⬜ | | | | |
|
||||
| 3.2.6 | Implement SVG path parsing via `path_drawing` into immutable `Path` objects at app launch | ⬜ | | | | |
|
||||
| 3.2.7 | Implement thread-safe LRU cache for parsed `Path` objects (indexed by SIDC) | ⬜ | | | | |
|
||||
| 3.2.8 | Implement `SidcResolver` mapping `(DoctrinalBranch, Echelon, Affiliation)` → 15-digit SIDC | ⬜ | | | | |
|
||||
| 3.2.9 | Implement `TacticalSymbolPainter`: Frame + Icon + Echelon Modifier + Text Designator | ⬜ | | | | |
|
||||
| 3.2.10 | Integrate `TacticalSymbolPainter` into main `UtoCanvasPainter` node rendering loop | ⬜ | | | | |
|
||||
|
||||
**3.2 Acceptance Criteria:**
|
||||
|
||||
- [ ] All defined branch/echelon combinations render correct MIL-STD-2525D symbols
|
||||
- [ ] Symbol cache hit rate > 95% during normal canvas rendering
|
||||
- [ ] Symbol rendering adds < 2ms overhead per frame on Android
|
||||
- [ ] Web target renders symbols via milsymbol.js without visible quality difference from native
|
||||
|
||||
---
|
||||
|
||||
### 3.3 — Three-Pane UI Shell
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 3.3.1 | Implement application header (Echelon Selector, Doctrinal Mode, Export Tools) | ⬜ | | | | |
|
||||
| 3.3.2 | Implement left panel: collapsible template palette sidebar | ⬜ | | | | |
|
||||
| 3.3.3 | Implement left panel: template drag sources (MBCT, SBCT, ABCT, CAB, echelon blocks, duty slots) | ⬜ | | | | |
|
||||
| 3.3.4 | Implement central canvas workspace integration (canvas + InteractiveViewer) | ⬜ | | | | |
|
||||
| 3.3.5 | Implement right panel: context-sensitive node inspector | ⬜ | | | | |
|
||||
| 3.3.6 | Implement right panel: unit ID, MOS, personnel search, weapon assignment, readiness status | ⬜ | | | | |
|
||||
| 3.3.7 | Implement status bar (scale factor, active node count, MTOE validation status) | ⬜ | | | | |
|
||||
| 3.3.8 | Implement keyboard shortcuts: `Ctrl+Z`, `Ctrl+Y`, `Ctrl+F`, `Ctrl+D`, `Ctrl+S` | ⬜ | | | | |
|
||||
| 3.3.9 | Implement mouse-wheel pan/zoom | ⬜ | | | | |
|
||||
| 3.3.10 | Implement multi-node box selection | ⬜ | | | | |
|
||||
| 3.3.11 | Implement right-click context menus (desktop) | ⬜ | | | | |
|
||||
| 3.3.12 | Implement Android navigation drawer (replaces left panel) | ⬜ | | | | |
|
||||
| 3.3.13 | Implement Android bottom sheet inspector (replaces right panel) | ⬜ | | | | |
|
||||
| 3.3.14 | Implement tap-and-hold radial menus (Android, replaces right-click) | ⬜ | | | | |
|
||||
| 3.3.15 | Verify all touch targets ≥ 48×48 dp on Android | ⬜ | | | | |
|
||||
|
||||
**3.3 Acceptance Criteria:**
|
||||
|
||||
- [ ] Three-pane layout renders correctly at 1920×1080 on Windows
|
||||
- [ ] Three-pane layout renders correctly at 2560×1440 on Windows
|
||||
- [ ] Mobile layout correctly switches to drawer/bottom-sheet on Android
|
||||
- [ ] All keyboard shortcuts function correctly on Windows and Web
|
||||
- [ ] Touch targets meet 48×48 dp minimum on Android
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 Exit Criteria
|
||||
|
||||
| Criterion | Status | Verified By | Date |
|
||||
|---|---|---|---|
|
||||
| All five BCT doctrinal templates instantiable from JSON | ⬜ | | |
|
||||
| MIL-STD-2525D symbology rendering on all three platforms | ⬜ | | |
|
||||
| Three-pane UI shell functional on desktop and adaptive on Android | ⬜ | | |
|
||||
| Runtime template reload mechanism operational | ⬜ | | |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Persistence, Validation Rules, & Export Engine
|
||||
|
||||
### Weeks 15 – 19 | Status: ⬜ NOT STARTED
|
||||
|
||||
> **Dependency:** Phase 3 must be complete before Phase 4 begins.
|
||||
|
||||
---
|
||||
|
||||
### 4.1 — Full Persistence Integration & Offline-First Architecture
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 4.1.1 | Complete Drift repository implementations for all four tables | ⬜ | | | | |
|
||||
| 4.1.2 | Implement reactive Drift `watchAll()` streams feeding BLoC state | ⬜ | | | | |
|
||||
| 4.1.3 | Implement `CommandEventStore` with local offline queue | ⬜ | | | | |
|
||||
| 4.1.4 | Implement `AttachNodeEvent` command event type | ⬜ | | | | |
|
||||
| 4.1.5 | Implement `DetachNodeEvent` command event type | ⬜ | | | | |
|
||||
| 4.1.6 | Implement `AssignPersonnelEvent` command event type | ⬜ | | | | |
|
||||
| 4.1.7 | Implement `UnassignPersonnelEvent` command event type | ⬜ | | | | |
|
||||
| 4.1.8 | Implement `ModifySlotEvent` command event type | ⬜ | | | | |
|
||||
| 4.1.9 | Implement `DeltaSynchronizer` (transmit queued events on reconnection) | ⬜ | | | | |
|
||||
| 4.1.10 | Implement conflict resolution strategy (last-write-wins + conflict flagging) | ⬜ | | | | |
|
||||
| 4.1.11 | Configure `drift_web` for IndexedDB persistence on Web target | ⬜ | | | | |
|
||||
| 4.1.12 | Implement database backup/restore (export to encrypted JSON archive) | ⬜ | | | | |
|
||||
|
||||
**4.1 Acceptance Criteria:**
|
||||
|
||||
- [ ] Application continues full operation with zero network connectivity
|
||||
- [ ] Delta synchronizer transmits only changed events (not full model) on reconnection
|
||||
- [ ] Reactive streams update UI within 16ms of database write
|
||||
- [ ] Backup/restore round-trip produces identical database state
|
||||
|
||||
---
|
||||
|
||||
### 4.2 — Real-Time MTOE Domain Validation Engine
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 4.2.1 | Implement `MtoeValidationEngine` in background Dart Isolate | ⬜ | | | | |
|
||||
| 4.2.2 | Implement MOS Mismatch Detection rule | ⬜ | | | | |
|
||||
| 4.2.3 | Implement Rank Mismatch Detection rule | ⬜ | | | | |
|
||||
| 4.2.4 | Implement Vacant Leadership Slot Detection rule | ⬜ | | | | |
|
||||
| 4.2.5 | Implement Missing Critical Support Role Detection rule (68W, 13F) | ⬜ | | | | |
|
||||
| 4.2.6 | Implement Structural Hierarchy Violation Detection rule | ⬜ | | | | |
|
||||
| 4.2.7 | Implement Doctrinal Strength Deviation rule (< 70% manning) | ⬜ | | | | |
|
||||
| 4.2.8 | Implement `ValidationFinding` result model (severity, unitId, slotId, findingCode, description) | ⬜ | | | | |
|
||||
| 4.2.9 | Integrate validation findings into status bar (aggregate counts by severity) | ⬜ | | | | |
|
||||
| 4.2.10 | Integrate validation findings into node visual indicators (colored border/badge) | ⬜ | | | | |
|
||||
| 4.2.11 | Integrate validation findings into right panel inspector (detailed finding list) | ⬜ | | | | |
|
||||
| 4.2.12 | Implement drag-and-drop pre-validation (evaluate before drop execution) | ⬜ | | | | |
|
||||
|
||||
**4.2 Acceptance Criteria:**
|
||||
|
||||
- [ ] All six validation rules correctly identify violations in test unit trees
|
||||
- [ ] Validation engine processes a full BCT (500+ nodes) in < 500ms in background Isolate
|
||||
- [ ] Node visual indicators update within one frame of validation completion
|
||||
- [ ] Dragging Fire Team to Brigade HQ shows red pre-validation indicator before drop
|
||||
|
||||
---
|
||||
|
||||
### 4.3 — Data Export Engine
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 4.3.1 | Implement JSON export module (full unit tree serialization) | ⬜ | | | | |
|
||||
| 4.3.2 | Implement Protobuf export module (binary-encoded unit tree) | ⬜ | | | | |
|
||||
| 4.3.3 | Implement vector PDF export: org chart rendering via `pdf` package | ⬜ | | | | |
|
||||
| 4.3.4 | Implement PDF export: MIL-STD-2525D symbols as vector paths | ⬜ | | | | |
|
||||
| 4.3.5 | Implement PDF export: configurable page sizes (Letter, A4, Tabloid/A3) | ⬜ | | | | |
|
||||
| 4.3.6 | Implement PDF export: cover page with unit designation, date, classification placeholder | ⬜ | | | | |
|
||||
| 4.3.7 | Implement JSON import pipeline with doctrinal validation | ⬜ | | | | |
|
||||
| 4.3.8 | Implement export UI dialog (format selection, scope selection, file save) | ⬜ | | | | |
|
||||
| 4.3.9 | Offload JSON/Protobuf serialization to background Dart Isolate | ⬜ | | | | |
|
||||
| 4.3.10 | Offload PDF rendering to background Dart Isolate | ⬜ | | | | |
|
||||
|
||||
**4.3 Acceptance Criteria:**
|
||||
|
||||
- [ ] JSON export/import round-trip produces identical unit tree
|
||||
- [ ] Protobuf export file size < 50% of equivalent JSON for a full BCT
|
||||
- [ ] PDF export renders all MIL-STD-2525D symbols as vector paths (not rasterized)
|
||||
- [ ] PDF export completes in < 10 seconds for a full BCT on Windows
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 Exit Criteria
|
||||
|
||||
| Criterion | Status | Verified By | Date |
|
||||
|---|---|---|---|
|
||||
| Full reactive Drift persistence layer operational on all three platforms | ⬜ | | |
|
||||
| Offline-first operation verified with delta synchronization | ⬜ | | |
|
||||
| MTOE validation engine detecting all defined violation types in real time | ⬜ | | |
|
||||
| JSON, Protobuf, and vector PDF export modules functional | ⬜ | | |
|
||||
| JSON import pipeline with doctrinal validation | ⬜ | | |
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Platform Optimization, Profiling, & Hardening
|
||||
|
||||
### Weeks 20 – 24 | Status: ⬜ NOT STARTED
|
||||
|
||||
> **Dependency:** Phase 4 must be complete before Phase 5 begins.
|
||||
|
||||
---
|
||||
|
||||
### 5.1 — Performance Profiling & Rendering Optimization
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 5.1.1 | Profile Windows target: frame timeline, memory, CPU via Flutter DevTools | ⬜ | | | | |
|
||||
| 5.1.2 | Profile Web (WASM) target: frame timeline, memory, CPU via Flutter DevTools | ⬜ | | | | |
|
||||
| 5.1.3 | Profile Android target: frame timeline, memory, CPU via Flutter DevTools | ⬜ | | | | |
|
||||
| 5.1.4 | Apply `RepaintBoundary` isolation: static layer (grid, connection lines) | ⬜ | | | | |
|
||||
| 5.1.5 | Apply `RepaintBoundary` isolation: dynamic layer (drag preview overlay) | ⬜ | | | | |
|
||||
| 5.1.6 | Apply `RepaintBoundary` isolation: node layer (individual unit nodes) | ⬜ | | | | |
|
||||
| 5.1.7 | Optimize Reingold-Tilford: cache subtree widths, skip unchanged subtrees | ⬜ | | | | |
|
||||
| 5.1.8 | Optimize Quadtree: bulk-insert for template instantiation, lazy rebuild | ⬜ | | | | |
|
||||
| 5.1.9 | Tune LRU symbol cache capacity per platform (Windows: 2,000 / Android: 500 / Web: 1,000) | ⬜ | | | | |
|
||||
| 5.1.10 | Implement progressive loading for large templates (visible viewport first) | ⬜ | | | | |
|
||||
| 5.1.11 | Build automated performance benchmark suite (frame rate tests at target node counts) | ⬜ | | | | |
|
||||
|
||||
**5.1 Acceptance Criteria:**
|
||||
|
||||
- [ ] Windows: 60 FPS at 5,000+ nodes (verified by automated benchmark)
|
||||
- [ ] Web (WASM): 60 FPS at 2,000+ nodes (verified by automated benchmark)
|
||||
- [ ] Android: 60 FPS at 1,500+ nodes (verified by automated benchmark)
|
||||
- [ ] No memory leaks over 30-minute continuous operation sessions
|
||||
- [ ] Main thread frame time < 8ms during layout recalculation
|
||||
|
||||
---
|
||||
|
||||
### 5.2 — Heavy Computation Isolate Offloading
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 5.2.1 | Audit and confirm: Reingold-Tilford layout → background Isolate | ⬜ | | | | |
|
||||
| 5.2.2 | Audit and confirm: MTOE validation engine → background Isolate | ⬜ | | | | |
|
||||
| 5.2.3 | Offload JSON/Protobuf serialization → background Isolate | ⬜ | | | | |
|
||||
| 5.2.4 | Offload PDF rendering → background Isolate | ⬜ | | | | |
|
||||
| 5.2.5 | Implement `IsolatePool` for managing concurrent background computations | ⬜ | | | | |
|
||||
| 5.2.6 | Implement progress reporting from Isolates to UI | ⬜ | | | | |
|
||||
| 5.2.7 | Implement Isolate cancellation for superseded layout requests | ⬜ | | | | |
|
||||
|
||||
**5.2 Acceptance Criteria:**
|
||||
|
||||
- [ ] Main UI thread frame time remains < 8ms during any background computation
|
||||
- [ ] Isolate cancellation correctly aborts superseded layout requests
|
||||
- [ ] Progress indicators display for all operations > 500ms
|
||||
|
||||
---
|
||||
|
||||
### 5.3 — Platform-Specific Hardening
|
||||
|
||||
Windows x64
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 5.3.1 | Implement multi-window support (multiple UTO workspaces simultaneously) | ⬜ | | | | |
|
||||
| 5.3.2 | Verify high-DPI scaling at 100%, 125%, 150%, 200% display scaling | ⬜ | | | | |
|
||||
| 5.3.3 | Implement system tray integration and window state persistence | ⬜ | | | | |
|
||||
| 5.3.4 | Configure `.msi` installer with registry entries and uninstall support | ⬜ | | | | |
|
||||
| 5.3.5 | Implement Windows file association for `.uto` project files | ⬜ | | | | |
|
||||
|
||||
Web (WASM/CanvasKit)
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 5.3.6 | Optimize WASM bundle size (tree-shaking, deferred template asset loading) | ⬜ | | | | |
|
||||
| 5.3.7 | Implement IndexedDB storage quota management and user warnings | ⬜ | | | | |
|
||||
| 5.3.8 | Implement memory-bounded canvas surface allocation | ⬜ | | | | |
|
||||
| 5.3.9 | Configure service worker for offline PWA capability | ⬜ | | | | |
|
||||
| 5.3.10 | Test across Chrome, Edge, and Firefox | ⬜ | | | | |
|
||||
|
||||
Android
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 5.3.11 | Implement battery profiling: reduce Isolate activity when battery < 20% | ⬜ | | | | |
|
||||
| 5.3.12 | Verify all touch targets ≥ 48×48 dp across all interactive elements | ⬜ | | | | |
|
||||
| 5.3.13 | Implement Android back-gesture handling for panel navigation | ⬜ | | | | |
|
||||
| 5.3.14 | Configure `.aab` release build with ProGuard/R8 optimization | ⬜ | | | | |
|
||||
| 5.3.15 | Test on Android 12, 13, and 14 (tablet and phone form factors) | ⬜ | | | | |
|
||||
|
||||
**5.3 Acceptance Criteria:**
|
||||
|
||||
- [ ] Windows installer installs/uninstalls cleanly; `.uto` file association works
|
||||
- [ ] Web app functions offline after initial load (PWA service worker active)
|
||||
- [ ] Android app passes battery profiling: no excessive background drain
|
||||
- [ ] All touch targets verified ≥ 48×48 dp on Android
|
||||
|
||||
---
|
||||
|
||||
### 5.4 — Security, Accessibility, & Final QA
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 5.4.1 | Implement classification marking placeholder system (UNCLASSIFIED / FOUO banners) | ⬜ | | | | |
|
||||
| 5.4.2 | Integrate SQLCipher for data-at-rest database encryption | ⬜ | | | | |
|
||||
| 5.4.3 | Implement platform keychain key storage (Windows Credential Manager / Android Keystore) | ⬜ | | | | |
|
||||
| 5.4.4 | Conduct screen reader compatibility audit (Windows Narrator, Android TalkBack) | ⬜ | | | | |
|
||||
| 5.4.5 | Verify WCAG 2.1 AA contrast ratios for all text elements | ⬜ | | | | |
|
||||
| 5.4.6 | Verify keyboard-only navigation completeness on Windows/Web | ⬜ | | | | |
|
||||
| 5.4.7 | Execute E2E test: template instantiation → drag-and-drop → personnel assignment → export | ⬜ | | | | |
|
||||
| 5.4.8 | Execute E2E test: offline operation → reconnection → delta synchronization | ⬜ | | | | |
|
||||
| 5.4.9 | Execute E2E test: validation finding detection → resolution → re-validation | ⬜ | | | | |
|
||||
| 5.4.10 | Conduct load test: 10 simultaneous full BCT templates, 10-minute stability | ⬜ | | | | |
|
||||
|
||||
**5.4 Acceptance Criteria:**
|
||||
|
||||
- [ ] Database encryption verified: raw SQLite file unreadable without key
|
||||
- [ ] All WCAG 2.1 AA contrast requirements met
|
||||
- [ ] All E2E tests pass with 100% success rate on all three platforms
|
||||
- [ ] Load test: 10 simultaneous BCT templates stable for 10 minutes without crash or memory leak
|
||||
|
||||
---
|
||||
|
||||
### 5.5 — Release Package Assembly
|
||||
|
||||
| # | Task | Status | Assignee | Start Date | End Date | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 5.5.1 | Assemble Windows release: signed `.msi` installer + portable `.exe` archive | ⬜ | | | | |
|
||||
| 5.5.2 | Assemble Web release: optimized WASM bundle + static hosting configuration | ⬜ | | | | |
|
||||
| 5.5.3 | Assemble Android release: signed `.aab` (Google Play) + sideload `.apk` | ⬜ | | | | |
|
||||
| 5.5.4 | Generate release notes (features, known limitations, upgrade instructions) | ⬜ | | | | |
|
||||
| 5.5.5 | Tag release in version control (`v1.0.0`) | ⬜ | | | | |
|
||||
| 5.5.6 | Archive build artifacts with SHA-256 checksums | ⬜ | | | | |
|
||||
|
||||
**5.5 Acceptance Criteria:**
|
||||
|
||||
- [ ] All three release packages install and launch correctly on clean target systems
|
||||
- [ ] SHA-256 checksums verified for all artifacts
|
||||
- [ ] Release tagged in version control
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 Exit Criteria
|
||||
|
||||
| Criterion | Status | Verified By | Date |
|
||||
|---|---|---|---|
|
||||
| All three platforms meet FPS targets at specified node counts | ⬜ | | |
|
||||
| All heavy computations confirmed offloaded to background Isolates | ⬜ | | |
|
||||
| Platform-specific hardening complete (Windows multi-window, Web PWA, Android battery) | ⬜ | | |
|
||||
| Database encryption integrated | ⬜ | | |
|
||||
| End-to-end test suite passing on all three platforms | ⬜ | | |
|
||||
| Release packages assembled and signed for all three platforms | ⬜ | | |
|
||||
|
||||
---
|
||||
|
||||
## Blockers & Issues Log
|
||||
|
||||
| # | Date Raised | Phase | Task # | Description | Severity | Status | Resolution | Date Resolved |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| — | — | — | — | No blockers recorded | — | — | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Milestone Summary
|
||||
|
||||
| Milestone | Target Week | Deliverable | Status |
|
||||
|---|---|---|---|
|
||||
| M1 | End of Week 4 | Compilable multi-platform skeleton + Drift DB + CI pipeline | ⬜ |
|
||||
| M2 | End of Week 9 | Functional canvas engine with drag-and-drop on Windows and Android | ⬜ |
|
||||
| M3 | End of Week 14 | All BCT templates instantiable + MIL-STD-2525D symbols rendering | ⬜ |
|
||||
| M4 | End of Week 19 | Full persistence + real-time MTOE validation + JSON/PDF export | ⬜ |
|
||||
| M5 | End of Week 24 | Signed release packages for Windows, Web, and Android | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## Task Count Summary
|
||||
|
||||
| Phase | Total Tasks | Complete | In Progress | Blocked | Not Started |
|
||||
|---|---|---|---|---|---|
|
||||
| Phase 1 | 30 | 0 | 0 | 0 | 30 |
|
||||
| Phase 2 | 29 | 0 | 0 | 0 | 29 |
|
||||
| Phase 3 | 34 | 0 | 0 | 0 | 34 |
|
||||
| Phase 4 | 30 | 0 | 0 | 0 | 30 |
|
||||
| Phase 5 | 33 | 0 | 0 | 0 | 33 |
|
||||
| **Total** | **156** | **0** | **0** | **0** | **156** |
|
||||
|
||||
---
|
||||
|
||||
Document Version: 1.0 | Generated: 2026-08-07 | Companion to: phased_development_plan.md
|
||||
|
||||
Update this document at the start and end of each work session.
|
||||
| Branch | Merged | Description |
|
||||
|--------|--------|-------------|
|
||||
| `feature/phase-1-foundations` | 2026-08-07 | Phase 1: Architecture & Data Foundations |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
/// Represents authorized pay grades in the U.S. Army.
|
||||
///
|
||||
/// Covers Enlisted (E1–E9), Warrant Officer (W1–W5), and
|
||||
/// Commissioned Officer (O1–O10) grades.
|
||||
enum AuthorizedRank {
|
||||
// Enlisted
|
||||
e1,
|
||||
e2,
|
||||
e3,
|
||||
e4,
|
||||
e5,
|
||||
e6,
|
||||
e7,
|
||||
e8,
|
||||
e9,
|
||||
// Warrant Officers
|
||||
w1,
|
||||
w2,
|
||||
w3,
|
||||
w4,
|
||||
w5,
|
||||
// Commissioned Officers
|
||||
o1,
|
||||
o2,
|
||||
o3,
|
||||
o4,
|
||||
o5,
|
||||
o6,
|
||||
o7,
|
||||
o8,
|
||||
o9,
|
||||
o10;
|
||||
|
||||
/// Returns the pay grade string (e.g., "E-4", "W-2", "O-3").
|
||||
String get payGrade {
|
||||
final name = this.name.toUpperCase();
|
||||
return '${name[0]}-${name.substring(1)}';
|
||||
}
|
||||
|
||||
/// Returns the common rank title for this pay grade.
|
||||
String get rankTitle {
|
||||
switch (this) {
|
||||
case AuthorizedRank.e1:
|
||||
return 'Private';
|
||||
case AuthorizedRank.e2:
|
||||
return 'Private Second Class';
|
||||
case AuthorizedRank.e3:
|
||||
return 'Private First Class';
|
||||
case AuthorizedRank.e4:
|
||||
return 'Specialist / Corporal';
|
||||
case AuthorizedRank.e5:
|
||||
return 'Sergeant';
|
||||
case AuthorizedRank.e6:
|
||||
return 'Staff Sergeant';
|
||||
case AuthorizedRank.e7:
|
||||
return 'Sergeant First Class';
|
||||
case AuthorizedRank.e8:
|
||||
return 'Master Sergeant / First Sergeant';
|
||||
case AuthorizedRank.e9:
|
||||
return 'Sergeant Major / Command Sergeant Major';
|
||||
case AuthorizedRank.w1:
|
||||
return 'Warrant Officer 1';
|
||||
case AuthorizedRank.w2:
|
||||
return 'Chief Warrant Officer 2';
|
||||
case AuthorizedRank.w3:
|
||||
return 'Chief Warrant Officer 3';
|
||||
case AuthorizedRank.w4:
|
||||
return 'Chief Warrant Officer 4';
|
||||
case AuthorizedRank.w5:
|
||||
return 'Chief Warrant Officer 5';
|
||||
case AuthorizedRank.o1:
|
||||
return 'Second Lieutenant';
|
||||
case AuthorizedRank.o2:
|
||||
return 'First Lieutenant';
|
||||
case AuthorizedRank.o3:
|
||||
return 'Captain';
|
||||
case AuthorizedRank.o4:
|
||||
return 'Major';
|
||||
case AuthorizedRank.o5:
|
||||
return 'Lieutenant Colonel';
|
||||
case AuthorizedRank.o6:
|
||||
return 'Colonel';
|
||||
case AuthorizedRank.o7:
|
||||
return 'Brigadier General';
|
||||
case AuthorizedRank.o8:
|
||||
return 'Major General';
|
||||
case AuthorizedRank.o9:
|
||||
return 'Lieutenant General';
|
||||
case AuthorizedRank.o10:
|
||||
return 'General';
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this rank is an enlisted grade.
|
||||
bool get isEnlisted => name.startsWith('e');
|
||||
|
||||
/// Returns true if this rank is a warrant officer grade.
|
||||
bool get isWarrantOfficer => name.startsWith('w');
|
||||
|
||||
/// Returns true if this rank is a commissioned officer grade.
|
||||
bool get isCommissionedOfficer => name.startsWith('o');
|
||||
|
||||
/// Returns the numeric grade level (1–10 within each category).
|
||||
int get gradeLevel => int.parse(name.substring(1));
|
||||
|
||||
/// Returns true if this rank meets or exceeds [minimum].
|
||||
bool meetsMinimum(AuthorizedRank minimum) {
|
||||
if (isEnlisted && minimum.isEnlisted) {
|
||||
return gradeLevel >= minimum.gradeLevel;
|
||||
}
|
||||
if (isWarrantOfficer && minimum.isWarrantOfficer) {
|
||||
return gradeLevel >= minimum.gradeLevel;
|
||||
}
|
||||
if (isCommissionedOfficer && minimum.isCommissionedOfficer) {
|
||||
return gradeLevel >= minimum.gradeLevel;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/// Represents the functional branch/specialty of a military unit or position.
|
||||
///
|
||||
/// Based on U.S. Army 2026 doctrine functional branch designations.
|
||||
enum DoctrinalBranch {
|
||||
infantry,
|
||||
medical,
|
||||
fires,
|
||||
signal,
|
||||
cyberEw,
|
||||
aviation,
|
||||
logistics,
|
||||
engineer;
|
||||
|
||||
/// Returns a human-readable display name for this branch.
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case DoctrinalBranch.infantry:
|
||||
return 'Infantry & Maneuver';
|
||||
case DoctrinalBranch.medical:
|
||||
return 'Medical (CMF 68)';
|
||||
case DoctrinalBranch.fires:
|
||||
return 'Field Artillery (CMF 13)';
|
||||
case DoctrinalBranch.signal:
|
||||
return 'Signal (CMF 25)';
|
||||
case DoctrinalBranch.cyberEw:
|
||||
return 'Cyber/EW (CMF 17)';
|
||||
case DoctrinalBranch.aviation:
|
||||
return 'Aviation';
|
||||
case DoctrinalBranch.logistics:
|
||||
return 'Logistics (CMF 91/92)';
|
||||
case DoctrinalBranch.engineer:
|
||||
return 'Engineer (CMF 12)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the short branch abbreviation.
|
||||
String get abbreviation {
|
||||
switch (this) {
|
||||
case DoctrinalBranch.infantry:
|
||||
return 'IN';
|
||||
case DoctrinalBranch.medical:
|
||||
return 'MC';
|
||||
case DoctrinalBranch.fires:
|
||||
return 'FA';
|
||||
case DoctrinalBranch.signal:
|
||||
return 'SC';
|
||||
case DoctrinalBranch.cyberEw:
|
||||
return 'CY';
|
||||
case DoctrinalBranch.aviation:
|
||||
return 'AV';
|
||||
case DoctrinalBranch.logistics:
|
||||
return 'LG';
|
||||
case DoctrinalBranch.engineer:
|
||||
return 'EN';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/// Domain entities barrel export.
|
||||
///
|
||||
/// Import this file to access all domain entity types.
|
||||
library;
|
||||
|
||||
export 'authorized_rank.dart';
|
||||
export 'doctrinal_branch.dart';
|
||||
export 'echelon.dart';
|
||||
export 'personnel_record.dart';
|
||||
export 'position_slot.dart';
|
||||
export 'unit_node.dart';
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/// Represents the hierarchical echelon levels in U.S. Army doctrine.
|
||||
///
|
||||
/// Ordered from highest (Brigade) to lowest (Team) echelon.
|
||||
enum Echelon {
|
||||
brigade,
|
||||
battalion,
|
||||
company,
|
||||
platoon,
|
||||
section,
|
||||
squad,
|
||||
team;
|
||||
|
||||
/// Returns a human-readable display name for this echelon.
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case Echelon.brigade:
|
||||
return 'Brigade';
|
||||
case Echelon.battalion:
|
||||
return 'Battalion';
|
||||
case Echelon.company:
|
||||
return 'Company';
|
||||
case Echelon.platoon:
|
||||
return 'Platoon';
|
||||
case Echelon.section:
|
||||
return 'Section';
|
||||
case Echelon.squad:
|
||||
return 'Squad';
|
||||
case Echelon.team:
|
||||
return 'Team';
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the numeric level (lower = higher echelon).
|
||||
int get level {
|
||||
switch (this) {
|
||||
case Echelon.brigade:
|
||||
return 1;
|
||||
case Echelon.battalion:
|
||||
return 2;
|
||||
case Echelon.company:
|
||||
return 3;
|
||||
case Echelon.platoon:
|
||||
return 4;
|
||||
case Echelon.section:
|
||||
return 5;
|
||||
case Echelon.squad:
|
||||
return 6;
|
||||
case Echelon.team:
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if [other] is a valid parent echelon for this echelon.
|
||||
///
|
||||
/// A valid parent must be exactly one level higher (lower level number).
|
||||
bool isValidParent(Echelon other) {
|
||||
return other.level == level - 1;
|
||||
}
|
||||
|
||||
/// Returns true if [other] is a valid child echelon for this echelon.
|
||||
bool isValidChild(Echelon other) {
|
||||
return other.level == level + 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
||||
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
||||
|
||||
/// Represents the readiness state of a soldier.
|
||||
enum ReadinessState {
|
||||
/// Fully mission-capable; no limiting factors.
|
||||
fullyMissionCapable,
|
||||
|
||||
/// Partially mission-capable; some limiting factors present.
|
||||
partiallyMissionCapable,
|
||||
|
||||
/// Not mission-capable; significant limiting factors.
|
||||
notMissionCapable,
|
||||
|
||||
/// Status unknown or not yet assessed.
|
||||
unknown;
|
||||
|
||||
/// Returns a human-readable display name.
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ReadinessState.fullyMissionCapable:
|
||||
return 'FMC';
|
||||
case ReadinessState.partiallyMissionCapable:
|
||||
return 'PMC';
|
||||
case ReadinessState.notMissionCapable:
|
||||
return 'NMC';
|
||||
case ReadinessState.unknown:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a soldier's personnel record in the UTO system.
|
||||
///
|
||||
/// Contains identifying information, MOS qualifications, pay grade,
|
||||
/// and current readiness state.
|
||||
class PersonnelRecord extends Equatable {
|
||||
const PersonnelRecord({
|
||||
required this.soldierId,
|
||||
required this.name,
|
||||
required this.primaryMos,
|
||||
required this.payGrade,
|
||||
required this.readinessState,
|
||||
this.qualifications = const [],
|
||||
this.assignedSlotId,
|
||||
});
|
||||
|
||||
/// Unique identifier for this soldier (UUID v4).
|
||||
final String soldierId;
|
||||
|
||||
/// The soldier's full name (Last, First MI format).
|
||||
final String name;
|
||||
|
||||
/// The soldier's primary Military Occupational Specialty.
|
||||
final DutyMos primaryMos;
|
||||
|
||||
/// The soldier's current pay grade.
|
||||
final AuthorizedRank payGrade;
|
||||
|
||||
/// Additional MOS qualifications and special skills.
|
||||
final List<String> qualifications;
|
||||
|
||||
/// The current readiness state of this soldier.
|
||||
final ReadinessState readinessState;
|
||||
|
||||
/// The ID of the position slot this soldier is assigned to, or null.
|
||||
final String? assignedSlotId;
|
||||
|
||||
/// Returns true if this soldier is currently assigned to a position.
|
||||
bool get isAssigned => assignedSlotId != null;
|
||||
|
||||
/// Returns true if this soldier is available for assignment.
|
||||
bool get isUnassigned => assignedSlotId == null;
|
||||
|
||||
/// Returns true if this soldier is fully mission capable.
|
||||
bool get isFmc => readinessState == ReadinessState.fullyMissionCapable;
|
||||
|
||||
/// Returns a copy of this [PersonnelRecord] with the given fields replaced.
|
||||
PersonnelRecord copyWith({
|
||||
String? soldierId,
|
||||
String? name,
|
||||
DutyMos? primaryMos,
|
||||
AuthorizedRank? payGrade,
|
||||
List<String>? qualifications,
|
||||
ReadinessState? readinessState,
|
||||
String? assignedSlotId,
|
||||
bool clearAssignment = false,
|
||||
}) {
|
||||
return PersonnelRecord(
|
||||
soldierId: soldierId ?? this.soldierId,
|
||||
name: name ?? this.name,
|
||||
primaryMos: primaryMos ?? this.primaryMos,
|
||||
payGrade: payGrade ?? this.payGrade,
|
||||
qualifications: qualifications ?? this.qualifications,
|
||||
readinessState: readinessState ?? this.readinessState,
|
||||
assignedSlotId: clearAssignment ? null : (assignedSlotId ?? this.assignedSlotId),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
soldierId,
|
||||
name,
|
||||
primaryMos,
|
||||
payGrade,
|
||||
qualifications,
|
||||
readinessState,
|
||||
assignedSlotId,
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() => 'PersonnelRecord(id: $soldierId, name: $name, '
|
||||
'mos: ${primaryMos.code}, grade: ${payGrade.payGrade}, '
|
||||
'readiness: ${readinessState.displayName})';
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
||||
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
||||
|
||||
/// Represents an authorized duty position slot within a unit.
|
||||
///
|
||||
/// A [PositionSlot] defines a specific billet in the unit's MTOE,
|
||||
/// including the required MOS, authorized rank, and whether it is
|
||||
/// a key leader position. A slot may or may not have an assigned soldier.
|
||||
class PositionSlot extends Equatable {
|
||||
const PositionSlot({
|
||||
required this.slotId,
|
||||
required this.unitId,
|
||||
required this.positionTitle,
|
||||
required this.dutyMos,
|
||||
required this.authorizedRank,
|
||||
required this.isKeyLeader,
|
||||
this.assignedPersonnelId,
|
||||
});
|
||||
|
||||
/// Unique identifier for this position slot (UUID v4).
|
||||
final String slotId;
|
||||
|
||||
/// The unit this slot belongs to.
|
||||
final String unitId;
|
||||
|
||||
/// The official position title (e.g., "Platoon Leader", "Squad Leader").
|
||||
final String positionTitle;
|
||||
|
||||
/// The authorized duty MOS for this position.
|
||||
final DutyMos dutyMos;
|
||||
|
||||
/// The authorized pay grade for this position.
|
||||
final AuthorizedRank authorizedRank;
|
||||
|
||||
/// Whether this is a key leader position (PL, PSG, SL, CO CDR, etc.).
|
||||
final bool isKeyLeader;
|
||||
|
||||
/// The ID of the assigned soldier, or null if the slot is vacant.
|
||||
final String? assignedPersonnelId;
|
||||
|
||||
/// Returns true if this slot has no assigned soldier.
|
||||
bool get isVacant => assignedPersonnelId == null;
|
||||
|
||||
/// Returns true if this slot is filled.
|
||||
bool get isFilled => assignedPersonnelId != null;
|
||||
|
||||
/// Returns a copy of this [PositionSlot] with the given fields replaced.
|
||||
PositionSlot copyWith({
|
||||
String? slotId,
|
||||
String? unitId,
|
||||
String? positionTitle,
|
||||
DutyMos? dutyMos,
|
||||
AuthorizedRank? authorizedRank,
|
||||
bool? isKeyLeader,
|
||||
String? assignedPersonnelId,
|
||||
bool clearAssignment = false,
|
||||
}) {
|
||||
return PositionSlot(
|
||||
slotId: slotId ?? this.slotId,
|
||||
unitId: unitId ?? this.unitId,
|
||||
positionTitle: positionTitle ?? this.positionTitle,
|
||||
dutyMos: dutyMos ?? this.dutyMos,
|
||||
authorizedRank: authorizedRank ?? this.authorizedRank,
|
||||
isKeyLeader: isKeyLeader ?? this.isKeyLeader,
|
||||
assignedPersonnelId:
|
||||
clearAssignment ? null : (assignedPersonnelId ?? this.assignedPersonnelId),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
slotId,
|
||||
unitId,
|
||||
positionTitle,
|
||||
dutyMos,
|
||||
authorizedRank,
|
||||
isKeyLeader,
|
||||
assignedPersonnelId,
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() => 'PositionSlot(id: $slotId, title: $positionTitle, '
|
||||
'mos: ${dutyMos.code}, rank: ${authorizedRank.payGrade}, '
|
||||
'vacant: $isVacant)';
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
|
||||
import 'package:uto_manager/domain/entities/echelon.dart';
|
||||
|
||||
/// Represents a single organizational unit node in the UTO hierarchy.
|
||||
///
|
||||
/// A [UnitNode] can represent any echelon from Brigade down to Team.
|
||||
/// Nodes form a tree structure via [parentUnitId] references.
|
||||
class UnitNode extends Equatable {
|
||||
const UnitNode({
|
||||
required this.unitId,
|
||||
required this.echelon,
|
||||
required this.branch,
|
||||
required this.name,
|
||||
required this.isKeyLeader,
|
||||
this.parentUnitId,
|
||||
});
|
||||
|
||||
/// Unique identifier for this unit (UUID v4).
|
||||
final String unitId;
|
||||
|
||||
/// The echelon level of this unit.
|
||||
final Echelon echelon;
|
||||
|
||||
/// The functional branch/specialty of this unit.
|
||||
final DoctrinalBranch branch;
|
||||
|
||||
/// The parent unit's ID, or null if this is a root node (Brigade HQ).
|
||||
final String? parentUnitId;
|
||||
|
||||
/// The display name of this unit (e.g., "1st Platoon", "Alpha Company").
|
||||
final String name;
|
||||
|
||||
/// Whether this node represents a key leader position.
|
||||
final bool isKeyLeader;
|
||||
|
||||
/// Returns true if this unit has no parent (is a root/top-level unit).
|
||||
bool get isRoot => parentUnitId == null;
|
||||
|
||||
/// Returns a copy of this [UnitNode] with the given fields replaced.
|
||||
UnitNode copyWith({
|
||||
String? unitId,
|
||||
Echelon? echelon,
|
||||
DoctrinalBranch? branch,
|
||||
String? parentUnitId,
|
||||
String? name,
|
||||
bool? isKeyLeader,
|
||||
bool clearParent = false,
|
||||
}) {
|
||||
return UnitNode(
|
||||
unitId: unitId ?? this.unitId,
|
||||
echelon: echelon ?? this.echelon,
|
||||
branch: branch ?? this.branch,
|
||||
parentUnitId: clearParent ? null : (parentUnitId ?? this.parentUnitId),
|
||||
name: name ?? this.name,
|
||||
isKeyLeader: isKeyLeader ?? this.isKeyLeader,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns true if [potentialParent] is a doctrinally valid parent for
|
||||
/// this unit based on echelon hierarchy rules.
|
||||
bool isValidParentNode(UnitNode potentialParent) {
|
||||
return echelon.isValidParent(potentialParent.echelon);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [unitId, echelon, branch, parentUnitId, name, isKeyLeader];
|
||||
|
||||
@override
|
||||
String toString() => 'UnitNode(id: $unitId, name: $name, '
|
||||
'echelon: ${echelon.displayName}, branch: ${branch.displayName})';
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
||||
import 'package:uto_manager/domain/entities/personnel_record.dart';
|
||||
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
||||
|
||||
/// Abstract repository interface for [PersonnelRecord] persistence operations.
|
||||
///
|
||||
/// Implementations are provided by the infrastructure layer. The domain layer
|
||||
/// depends only on this interface, never on concrete implementations.
|
||||
abstract interface class PersonnelRepository {
|
||||
/// Returns all personnel records in the database.
|
||||
Future<List<PersonnelRecord>> getAll();
|
||||
|
||||
/// Returns the personnel record with [soldierId], or null if not found.
|
||||
Future<PersonnelRecord?> getById(String soldierId);
|
||||
|
||||
/// Returns all soldiers assigned to the slot with [slotId].
|
||||
Future<List<PersonnelRecord>> getBySlot(String slotId);
|
||||
|
||||
/// Returns all unassigned soldiers (not assigned to any slot).
|
||||
Future<List<PersonnelRecord>> getUnassigned();
|
||||
|
||||
/// Returns all soldiers with the given [primaryMos].
|
||||
Future<List<PersonnelRecord>> getByMos(DutyMos primaryMos);
|
||||
|
||||
/// Returns all soldiers at the given [payGrade].
|
||||
Future<List<PersonnelRecord>> getByPayGrade(AuthorizedRank payGrade);
|
||||
|
||||
/// Inserts a new [record] into the repository.
|
||||
///
|
||||
/// Throws [StateError] if a record with the same soldier ID already exists.
|
||||
Future<void> insert(PersonnelRecord record);
|
||||
|
||||
/// Inserts multiple [records] in a single atomic transaction.
|
||||
Future<void> insertAll(List<PersonnelRecord> records);
|
||||
|
||||
/// Updates an existing [record] in the repository.
|
||||
///
|
||||
/// Throws [StateError] if no record with the given soldier ID exists.
|
||||
Future<void> update(PersonnelRecord record);
|
||||
|
||||
/// Deletes the personnel record with [soldierId].
|
||||
Future<void> delete(String soldierId);
|
||||
|
||||
/// Assigns the soldier with [soldierId] to the slot with [slotId].
|
||||
///
|
||||
/// Clears any previous slot assignment for this soldier.
|
||||
Future<void> assignToSlot(String soldierId, String slotId);
|
||||
|
||||
/// Removes the slot assignment for the soldier with [soldierId].
|
||||
Future<void> unassignFromSlot(String soldierId);
|
||||
|
||||
/// Returns a stream that emits the full personnel list whenever it changes.
|
||||
Stream<List<PersonnelRecord>> watchAll();
|
||||
|
||||
/// Returns a stream that emits unassigned soldiers whenever the list changes.
|
||||
Stream<List<PersonnelRecord>> watchUnassigned();
|
||||
|
||||
/// Deletes all personnel records from the repository.
|
||||
Future<void> deleteAll();
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:uto_manager/domain/entities/echelon.dart';
|
||||
import 'package:uto_manager/domain/entities/unit_node.dart';
|
||||
|
||||
/// Abstract repository interface for [UnitNode] persistence operations.
|
||||
///
|
||||
/// Implementations are provided by the infrastructure layer (Drift/SQLite
|
||||
/// for Windows/Android, IndexedDB for Web). The domain layer depends only
|
||||
/// on this interface, never on concrete implementations.
|
||||
abstract interface class UnitRepository {
|
||||
/// Returns all unit nodes in the database.
|
||||
Future<List<UnitNode>> getAll();
|
||||
|
||||
/// Returns the unit node with the given [unitId], or null if not found.
|
||||
Future<UnitNode?> getById(String unitId);
|
||||
|
||||
/// Returns all direct children of the unit with [parentUnitId].
|
||||
Future<List<UnitNode>> getChildren(String parentUnitId);
|
||||
|
||||
/// Returns all root units (units with no parent).
|
||||
Future<List<UnitNode>> getRoots();
|
||||
|
||||
/// Returns all units at the given [echelon] level.
|
||||
Future<List<UnitNode>> getByEchelon(Echelon echelon);
|
||||
|
||||
/// Returns the full subtree rooted at [unitId] (inclusive).
|
||||
Future<List<UnitNode>> getSubtree(String unitId);
|
||||
|
||||
/// Inserts a new [unit] into the repository.
|
||||
///
|
||||
/// Throws [StateError] if a unit with the same ID already exists.
|
||||
Future<void> insert(UnitNode unit);
|
||||
|
||||
/// Inserts multiple [units] in a single atomic transaction.
|
||||
Future<void> insertAll(List<UnitNode> units);
|
||||
|
||||
/// Updates an existing [unit] in the repository.
|
||||
///
|
||||
/// Throws [StateError] if no unit with the given ID exists.
|
||||
Future<void> update(UnitNode unit);
|
||||
|
||||
/// Deletes the unit with [unitId] from the repository.
|
||||
///
|
||||
/// Does not cascade-delete children; caller is responsible for
|
||||
/// re-parenting or deleting child units first.
|
||||
Future<void> delete(String unitId);
|
||||
|
||||
/// Deletes the unit with [unitId] and all its descendants.
|
||||
Future<void> deleteSubtree(String unitId);
|
||||
|
||||
/// Re-parents the unit with [unitId] to the unit with [newParentId].
|
||||
///
|
||||
/// Validates that the new parent echelon is compatible before executing.
|
||||
/// Throws [ArgumentError] if the re-parenting would violate doctrine.
|
||||
Future<void> reparent(String unitId, String newParentId);
|
||||
|
||||
/// Returns a stream that emits the full unit list whenever it changes.
|
||||
Stream<List<UnitNode>> watchAll();
|
||||
|
||||
/// Returns a stream that emits the subtree rooted at [unitId] on changes.
|
||||
Stream<List<UnitNode>> watchSubtree(String unitId);
|
||||
|
||||
/// Deletes all units from the repository (used for template reset).
|
||||
Future<void> deleteAll();
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
|
||||
|
||||
/// Represents a validated Military Occupational Specialty (MOS) code.
|
||||
///
|
||||
/// MOS codes follow the U.S. Army standard format: 2 digits + 1 letter
|
||||
/// (e.g., "11B", "68W", "13F"). This value object enforces that format
|
||||
/// and validates against the known MOS dictionary.
|
||||
@immutable
|
||||
class DutyMos {
|
||||
/// Creates a [DutyMos] from a raw MOS code string.
|
||||
///
|
||||
/// Throws [ArgumentError] if the code is not a valid 3-character MOS.
|
||||
factory DutyMos(String code) {
|
||||
final normalized = code.trim().toUpperCase();
|
||||
if (!_isValidFormat(normalized)) {
|
||||
throw ArgumentError(
|
||||
'Invalid MOS code format: "$code". '
|
||||
'Expected format: 2 digits + 1 letter (e.g., "11B", "68W").',
|
||||
);
|
||||
}
|
||||
if (!_knownMosCodes.contains(normalized)) {
|
||||
throw ArgumentError(
|
||||
'Unknown MOS code: "$normalized". '
|
||||
'Code is not in the authorized MOS dictionary.',
|
||||
);
|
||||
}
|
||||
return DutyMos._(normalized);
|
||||
}
|
||||
|
||||
/// Creates a [DutyMos] without dictionary validation (for testing/migration).
|
||||
factory DutyMos.unvalidated(String code) {
|
||||
final normalized = code.trim().toUpperCase();
|
||||
if (!_isValidFormat(normalized)) {
|
||||
throw ArgumentError(
|
||||
'Invalid MOS code format: "$code". '
|
||||
'Expected format: 2 digits + 1 letter (e.g., "11B", "68W").',
|
||||
);
|
||||
}
|
||||
return DutyMos._(normalized);
|
||||
}
|
||||
|
||||
const DutyMos._(this.code);
|
||||
|
||||
/// The normalized MOS code string (e.g., "11B", "68W", "13F").
|
||||
final String code;
|
||||
|
||||
/// Returns true if [code] matches the 2-digit + 1-letter MOS format.
|
||||
static bool _isValidFormat(String code) {
|
||||
if (code.length != 3) return false;
|
||||
final digits = code.substring(0, 2);
|
||||
final letter = code[2];
|
||||
return RegExp(r'^\d{2}$').hasMatch(digits) && RegExp(r'^[A-Z]$').hasMatch(letter);
|
||||
}
|
||||
|
||||
/// Returns true if [code] is a known MOS in the authorized dictionary.
|
||||
static bool isKnown(String code) {
|
||||
return _knownMosCodes.contains(code.trim().toUpperCase());
|
||||
}
|
||||
|
||||
/// Returns all MOS codes for a given [DoctrinalBranch].
|
||||
static Set<String> codesForBranch(DoctrinalBranch branch) {
|
||||
return _branchMosMap[branch] ?? {};
|
||||
}
|
||||
|
||||
/// The complete set of authorized MOS codes.
|
||||
static const Set<String> _knownMosCodes = {
|
||||
// Infantry & Maneuver (CMF 11 / 15)
|
||||
'11A', '11B', '11C', '15W', '15A', '15T', '15B', '15D', '15G', '153A',
|
||||
// Medical (CMF 68)
|
||||
'68W', '68A', '68D', '68E', '68F', '68G', '68J',
|
||||
'68K', '68P', '68Q', '68R', '68S', '68T', '68X',
|
||||
// Field Artillery (CMF 13)
|
||||
'13A', '13B', '13D', '13F', '13J', '13M', '13P', '13R', '13T',
|
||||
// Signal (CMF 25)
|
||||
'25A', '25B', '25C', '25D', '25E', '25H', '25L', '25M',
|
||||
'25N', '25P', '25Q', '25R', '25S', '25U', '25V', '25W', '25X', '25Z',
|
||||
// Cyber/EW (CMF 17)
|
||||
'17A', '17C', '17E', '17S',
|
||||
// Aviation (CMF 15)
|
||||
'15Z',
|
||||
// Logistics (CMF 91/92)
|
||||
'91A', '91B', '91C', '91D', '91E', '91F', '91G', '91H',
|
||||
'91J', '91L', '91M', '91P', '91S', '91X', '91Z',
|
||||
'92A', '92F', '92G', '92L', '92M', '92R', '92S', '92W', '92Y', '92Z',
|
||||
// Engineer (CMF 12)
|
||||
'12A', '12B', '12C', '12D', '12G', '12H', '12K', '12M',
|
||||
'12N', '12P', '12Q', '12R', '12T', '12W', '12X', '12Y', '12Z',
|
||||
};
|
||||
|
||||
/// Maps each [DoctrinalBranch] to its associated MOS codes.
|
||||
static const Map<DoctrinalBranch, Set<String>> _branchMosMap = {
|
||||
DoctrinalBranch.infantry: {
|
||||
'11A',
|
||||
'11B',
|
||||
'11C',
|
||||
'15W',
|
||||
'15A',
|
||||
'15T',
|
||||
'153A',
|
||||
},
|
||||
DoctrinalBranch.medical: {
|
||||
'68W',
|
||||
'68A',
|
||||
'68D',
|
||||
'68E',
|
||||
'68F',
|
||||
'68G',
|
||||
'68J',
|
||||
'68K',
|
||||
'68P',
|
||||
'68Q',
|
||||
'68R',
|
||||
'68S',
|
||||
'68T',
|
||||
'68X',
|
||||
},
|
||||
DoctrinalBranch.fires: {
|
||||
'13A',
|
||||
'13B',
|
||||
'13D',
|
||||
'13F',
|
||||
'13J',
|
||||
'13M',
|
||||
'13P',
|
||||
'13R',
|
||||
'13T',
|
||||
},
|
||||
DoctrinalBranch.signal: {
|
||||
'25A',
|
||||
'25B',
|
||||
'25C',
|
||||
'25D',
|
||||
'25E',
|
||||
'25H',
|
||||
'25L',
|
||||
'25M',
|
||||
'25N',
|
||||
'25P',
|
||||
'25Q',
|
||||
'25R',
|
||||
'25S',
|
||||
'25U',
|
||||
'25V',
|
||||
'25W',
|
||||
'25X',
|
||||
'25Z',
|
||||
},
|
||||
DoctrinalBranch.cyberEw: {'17A', '17C', '17E', '17S'},
|
||||
DoctrinalBranch.aviation: {
|
||||
'15A',
|
||||
'15B',
|
||||
'15D',
|
||||
'15G',
|
||||
'15T',
|
||||
'15W',
|
||||
'15Z',
|
||||
'153A',
|
||||
},
|
||||
DoctrinalBranch.logistics: {
|
||||
'91A',
|
||||
'91B',
|
||||
'91C',
|
||||
'91D',
|
||||
'91E',
|
||||
'91F',
|
||||
'91G',
|
||||
'91H',
|
||||
'91J',
|
||||
'91L',
|
||||
'91M',
|
||||
'91P',
|
||||
'91S',
|
||||
'91X',
|
||||
'91Z',
|
||||
'92A',
|
||||
'92F',
|
||||
'92G',
|
||||
'92L',
|
||||
'92M',
|
||||
'92R',
|
||||
'92S',
|
||||
'92W',
|
||||
'92Y',
|
||||
'92Z',
|
||||
},
|
||||
DoctrinalBranch.engineer: {
|
||||
'12A',
|
||||
'12B',
|
||||
'12C',
|
||||
'12D',
|
||||
'12G',
|
||||
'12H',
|
||||
'12K',
|
||||
'12M',
|
||||
'12N',
|
||||
'12P',
|
||||
'12Q',
|
||||
'12R',
|
||||
'12T',
|
||||
'12W',
|
||||
'12X',
|
||||
'12Y',
|
||||
'12Z',
|
||||
},
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || (other is DutyMos && other.code == code);
|
||||
|
||||
@override
|
||||
int get hashCode => code.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => code;
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table Definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Drift table for organizational unit nodes.
|
||||
class Units extends Table {
|
||||
TextColumn get unitId => text().named('unit_id')();
|
||||
TextColumn get echelon => text()();
|
||||
TextColumn get branch => text()();
|
||||
TextColumn get parentUnitId => text().named('parent_unit_id').nullable()();
|
||||
TextColumn get name => text()();
|
||||
BoolColumn get isKeyLeader => boolean().named('is_key_leader')();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {unitId};
|
||||
}
|
||||
|
||||
/// Drift table for authorized duty position slots.
|
||||
class PositionSlots extends Table {
|
||||
TextColumn get slotId => text().named('slot_id')();
|
||||
TextColumn get unitId => text().named('unit_id').references(Units, #unitId)();
|
||||
TextColumn get positionTitle => text().named('position_title')();
|
||||
TextColumn get dutyMos => text().named('duty_mos')();
|
||||
TextColumn get authorizedRank => text().named('authorized_rank')();
|
||||
BoolColumn get isKeyLeader => boolean().named('is_key_leader')();
|
||||
TextColumn get assignedPersonnelId => text().named('assigned_personnel_id').nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {slotId};
|
||||
}
|
||||
|
||||
/// Drift table for soldier personnel records.
|
||||
class Personnel extends Table {
|
||||
TextColumn get soldierId => text().named('soldier_id')();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get primaryMos => text().named('primary_mos')();
|
||||
TextColumn get payGrade => text().named('pay_grade')();
|
||||
TextColumn get qualifications => text()(); // JSON-encoded list
|
||||
TextColumn get readinessState => text().named('readiness_state')();
|
||||
TextColumn get assignedSlotId => text().named('assigned_slot_id').nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {soldierId};
|
||||
}
|
||||
|
||||
/// Drift table for serialized unit tree templates.
|
||||
class Templates extends Table {
|
||||
TextColumn get templateId => text().named('template_id')();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get bctType => text().named('bct_type')();
|
||||
TextColumn get jsonData => text().named('json_data')();
|
||||
DateTimeColumn get createdAt => dateTime().named('created_at')();
|
||||
DateTimeColumn get updatedAt => dateTime().named('updated_at')();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {templateId};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Database
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The main Drift database for the UTO Manager application.
|
||||
///
|
||||
/// Manages all four core tables: units, position_slots, personnel, templates.
|
||||
/// Supports SQLite on Windows/Android and IndexedDB on Web via drift_web.
|
||||
@DriftDatabase(tables: [Units, PositionSlots, Personnel, Templates])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
/// Creates the production database backed by SQLite on disk.
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
/// Constructor for in-memory database (used in tests).
|
||||
AppDatabase.forTesting(super.executor);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Future migration steps added here as schemaVersion increases.
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens the SQLite database connection for native platforms.
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'uto_manager.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const UtoManagerApp());
|
||||
}
|
||||
|
||||
/// The root application widget for the UTO Manager.
|
||||
///
|
||||
/// This is the entry point for the Flutter application. The full application
|
||||
/// shell (three-pane layout, canvas engine, state management) will be
|
||||
/// implemented in Phase 3.
|
||||
class UtoManagerApp extends StatelessWidget {
|
||||
const UtoManagerApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'UTO Manager',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF1B3A6B), // Army blue
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const _PlaceholderScaffold(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder scaffold displayed during Phase 1 development.
|
||||
///
|
||||
/// Will be replaced by the full three-pane UI shell in Phase 3.
|
||||
class _PlaceholderScaffold extends StatelessWidget {
|
||||
const _PlaceholderScaffold();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('UTO Manager'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.military_tech, size: 64),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Unit Task Organization Manager',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'U.S. Army 2026 Doctrine — '
|
||||
'Phase 1: Architecture & Data Foundations',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Text(
|
||||
'Canvas engine and UI shell coming in Phase 2 & 3.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,11 @@ This document breaks the master development plan into five structured phases spa
|
|||
|
||||
---
|
||||
|
||||
## Phase 1: Architecture & Data Foundations
|
||||
## Phase 1: Architecture & Data Foundations ✅ COMPLETE (2026-08-07)
|
||||
|
||||
> **Status:** All Phase 1 deliverables completed and merged to `main`.
|
||||
> 94/94 tests passing. `flutter analyze` — No issues found.
|
||||
> Branch: `feature/phase-1-foundations` → merged 2026-08-07.
|
||||
|
||||
### Weeks 1 – 4
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,789 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "100.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc
|
||||
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "94eaf6708fe64408c632ef2689ca3777b112f9421306ccf4f8c84d7c5c9f83f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "79e05eaf15a48d7230b053a4363b8eaac0cc234bbd0134c3229455481f55cbc6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.5"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.15.1"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.7"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: "71e43f1976c3eae2d9468a5b4d6600bf8cae61508b87f27fa1799228935d48ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.9"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "3a3f1f6f905037d7426e4c445854139fd6a3d592135f7c96d7931682b73d16f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.34.3"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "735aad3c34215805c66bd518c8812fa4f83b5534b2c13c4092c970c04a7e9983"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.34.5"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.6"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
jni_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_util
|
||||
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.12.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: e45aefa0324f08c683caafbb94b72837aa6193c61822799c916e45f4a263113d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.14.1"
|
||||
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: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.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: "direct main"
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.2"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "5e6f216fdf6376c9f3852381ae037499797a3385377d388b011dac98d303c67c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.13"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "64b2c63c8232dd20d14b34105a81ebfd74320442e8451f836179ec89986aa478"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.1"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.42"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "772bb2f6f5bce0631a60f26b57d6e8882e1105c22345b05c163ee6ee5d7ba32d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.45.0"
|
||||
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"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
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"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
very_good_analysis:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: very_good_analysis
|
||||
sha256: "1fb637c0022034b1f19ea2acb42a3603cbd8314a470646a59a2fb01f5f3a8629"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
name: uto_manager
|
||||
description: "Flutter-Based Military Unit Task Organization (UTO) Manager — U.S. Army 2026 Doctrine Cross-Platform Enterprise System"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.1.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.4.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
drift: ^2.20.3
|
||||
equatable: ^2.0.5
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.6
|
||||
json_annotation: ^4.9.0
|
||||
meta: ^1.18.0
|
||||
path: ^1.9.0
|
||||
path_provider: ^2.1.4
|
||||
sqlite3_flutter_libs: ^0.5.24
|
||||
uuid: ^4.5.1
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.12
|
||||
drift_dev: ^2.20.3
|
||||
flutter_lints: ^4.0.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
json_serializable: ^6.8.0
|
||||
very_good_analysis: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/data/
|
||||
- assets/svg/
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthorizedRank', () {
|
||||
group('payGrade', () {
|
||||
test('returns correct pay grade string for enlisted', () {
|
||||
expect(AuthorizedRank.e1.payGrade, equals('E-1'));
|
||||
expect(AuthorizedRank.e4.payGrade, equals('E-4'));
|
||||
expect(AuthorizedRank.e9.payGrade, equals('E-9'));
|
||||
});
|
||||
|
||||
test('returns correct pay grade string for warrant officers', () {
|
||||
expect(AuthorizedRank.w1.payGrade, equals('W-1'));
|
||||
expect(AuthorizedRank.w5.payGrade, equals('W-5'));
|
||||
});
|
||||
|
||||
test('returns correct pay grade string for commissioned officers', () {
|
||||
expect(AuthorizedRank.o1.payGrade, equals('O-1'));
|
||||
expect(AuthorizedRank.o6.payGrade, equals('O-6'));
|
||||
expect(AuthorizedRank.o10.payGrade, equals('O-10'));
|
||||
});
|
||||
});
|
||||
|
||||
group('category flags', () {
|
||||
test('enlisted ranks are correctly identified', () {
|
||||
expect(AuthorizedRank.e1.isEnlisted, isTrue);
|
||||
expect(AuthorizedRank.e9.isEnlisted, isTrue);
|
||||
expect(AuthorizedRank.w1.isEnlisted, isFalse);
|
||||
expect(AuthorizedRank.o1.isEnlisted, isFalse);
|
||||
});
|
||||
|
||||
test('warrant officer ranks are correctly identified', () {
|
||||
expect(AuthorizedRank.w1.isWarrantOfficer, isTrue);
|
||||
expect(AuthorizedRank.w5.isWarrantOfficer, isTrue);
|
||||
expect(AuthorizedRank.e1.isWarrantOfficer, isFalse);
|
||||
expect(AuthorizedRank.o1.isWarrantOfficer, isFalse);
|
||||
});
|
||||
|
||||
test('commissioned officer ranks are correctly identified', () {
|
||||
expect(AuthorizedRank.o1.isCommissionedOfficer, isTrue);
|
||||
expect(AuthorizedRank.o10.isCommissionedOfficer, isTrue);
|
||||
expect(AuthorizedRank.e1.isCommissionedOfficer, isFalse);
|
||||
expect(AuthorizedRank.w1.isCommissionedOfficer, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('gradeLevel', () {
|
||||
test('returns correct numeric grade level', () {
|
||||
expect(AuthorizedRank.e1.gradeLevel, equals(1));
|
||||
expect(AuthorizedRank.e4.gradeLevel, equals(4));
|
||||
expect(AuthorizedRank.e9.gradeLevel, equals(9));
|
||||
expect(AuthorizedRank.w1.gradeLevel, equals(1));
|
||||
expect(AuthorizedRank.w5.gradeLevel, equals(5));
|
||||
expect(AuthorizedRank.o1.gradeLevel, equals(1));
|
||||
expect(AuthorizedRank.o10.gradeLevel, equals(10));
|
||||
});
|
||||
});
|
||||
|
||||
group('meetsMinimum', () {
|
||||
test('E-6 meets minimum of E-5', () {
|
||||
expect(AuthorizedRank.e6.meetsMinimum(AuthorizedRank.e5), isTrue);
|
||||
});
|
||||
|
||||
test('E-6 meets minimum of E-6 (exact match)', () {
|
||||
expect(AuthorizedRank.e6.meetsMinimum(AuthorizedRank.e6), isTrue);
|
||||
});
|
||||
|
||||
test('E-4 does NOT meet minimum of E-5', () {
|
||||
expect(AuthorizedRank.e4.meetsMinimum(AuthorizedRank.e5), isFalse);
|
||||
});
|
||||
|
||||
test('W-3 meets minimum of W-2', () {
|
||||
expect(AuthorizedRank.w3.meetsMinimum(AuthorizedRank.w2), isTrue);
|
||||
});
|
||||
|
||||
test('O-3 meets minimum of O-2', () {
|
||||
expect(AuthorizedRank.o3.meetsMinimum(AuthorizedRank.o2), isTrue);
|
||||
});
|
||||
|
||||
test('enlisted does NOT meet minimum of officer (cross-category)', () {
|
||||
expect(AuthorizedRank.e9.meetsMinimum(AuthorizedRank.o1), isFalse);
|
||||
});
|
||||
|
||||
test('officer does NOT meet minimum of enlisted (cross-category)', () {
|
||||
expect(AuthorizedRank.o1.meetsMinimum(AuthorizedRank.e1), isFalse);
|
||||
});
|
||||
|
||||
test('warrant does NOT meet minimum of enlisted (cross-category)', () {
|
||||
expect(AuthorizedRank.w1.meetsMinimum(AuthorizedRank.e1), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('rankTitle', () {
|
||||
test('returns correct title for common ranks', () {
|
||||
expect(AuthorizedRank.e1.rankTitle, equals('Private'));
|
||||
expect(AuthorizedRank.e5.rankTitle, equals('Sergeant'));
|
||||
expect(AuthorizedRank.e6.rankTitle, equals('Staff Sergeant'));
|
||||
expect(AuthorizedRank.e7.rankTitle, equals('Sergeant First Class'));
|
||||
expect(AuthorizedRank.o2.rankTitle, equals('First Lieutenant'));
|
||||
expect(AuthorizedRank.o3.rankTitle, equals('Captain'));
|
||||
expect(AuthorizedRank.w1.rankTitle, equals('Warrant Officer 1'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:uto_manager/domain/entities/echelon.dart';
|
||||
|
||||
void main() {
|
||||
group('Echelon', () {
|
||||
group('displayName', () {
|
||||
test('returns correct display name for each echelon', () {
|
||||
expect(Echelon.brigade.displayName, equals('Brigade'));
|
||||
expect(Echelon.battalion.displayName, equals('Battalion'));
|
||||
expect(Echelon.company.displayName, equals('Company'));
|
||||
expect(Echelon.platoon.displayName, equals('Platoon'));
|
||||
expect(Echelon.section.displayName, equals('Section'));
|
||||
expect(Echelon.squad.displayName, equals('Squad'));
|
||||
expect(Echelon.team.displayName, equals('Team'));
|
||||
});
|
||||
});
|
||||
|
||||
group('level', () {
|
||||
test('brigade has the lowest level number (highest echelon)', () {
|
||||
expect(Echelon.brigade.level, equals(1));
|
||||
});
|
||||
|
||||
test('team has the highest level number (lowest echelon)', () {
|
||||
expect(Echelon.team.level, equals(7));
|
||||
});
|
||||
|
||||
test('levels are strictly ordered from brigade to team', () {
|
||||
final echelons = [
|
||||
Echelon.brigade,
|
||||
Echelon.battalion,
|
||||
Echelon.company,
|
||||
Echelon.platoon,
|
||||
Echelon.section,
|
||||
Echelon.squad,
|
||||
Echelon.team,
|
||||
];
|
||||
for (var i = 0; i < echelons.length - 1; i++) {
|
||||
expect(
|
||||
echelons[i].level,
|
||||
lessThan(echelons[i + 1].level),
|
||||
reason: '${echelons[i].displayName} should have lower level than '
|
||||
'${echelons[i + 1].displayName}',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('isValidParent', () {
|
||||
test('battalion is a valid parent for company', () {
|
||||
expect(Echelon.company.isValidParent(Echelon.battalion), isTrue);
|
||||
});
|
||||
|
||||
test('brigade is a valid parent for battalion', () {
|
||||
expect(Echelon.battalion.isValidParent(Echelon.brigade), isTrue);
|
||||
});
|
||||
|
||||
test('company is a valid parent for platoon', () {
|
||||
expect(Echelon.platoon.isValidParent(Echelon.company), isTrue);
|
||||
});
|
||||
|
||||
test('platoon is a valid parent for section', () {
|
||||
expect(Echelon.section.isValidParent(Echelon.platoon), isTrue);
|
||||
});
|
||||
|
||||
test('section is a valid parent for squad', () {
|
||||
expect(Echelon.squad.isValidParent(Echelon.section), isTrue);
|
||||
});
|
||||
|
||||
test('squad is a valid parent for team', () {
|
||||
expect(Echelon.team.isValidParent(Echelon.squad), isTrue);
|
||||
});
|
||||
|
||||
test('brigade is NOT a valid parent for company (skips battalion)', () {
|
||||
expect(Echelon.company.isValidParent(Echelon.brigade), isFalse);
|
||||
});
|
||||
|
||||
test('brigade is NOT a valid parent for platoon', () {
|
||||
expect(Echelon.platoon.isValidParent(Echelon.brigade), isFalse);
|
||||
});
|
||||
|
||||
test('same echelon is NOT a valid parent', () {
|
||||
expect(Echelon.platoon.isValidParent(Echelon.platoon), isFalse);
|
||||
});
|
||||
|
||||
test('child echelon is NOT a valid parent', () {
|
||||
expect(Echelon.brigade.isValidParent(Echelon.battalion), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('isValidChild', () {
|
||||
test('company is a valid child of battalion', () {
|
||||
expect(Echelon.battalion.isValidChild(Echelon.company), isTrue);
|
||||
});
|
||||
|
||||
test('battalion is a valid child of brigade', () {
|
||||
expect(Echelon.brigade.isValidChild(Echelon.battalion), isTrue);
|
||||
});
|
||||
|
||||
test('platoon is NOT a valid child of brigade', () {
|
||||
expect(Echelon.brigade.isValidChild(Echelon.platoon), isFalse);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
||||
import 'package:uto_manager/domain/entities/personnel_record.dart';
|
||||
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
||||
|
||||
void main() {
|
||||
group('PersonnelRecord', () {
|
||||
late PersonnelRecord soldier;
|
||||
|
||||
setUp(() {
|
||||
soldier = PersonnelRecord(
|
||||
soldierId: 'soldier-001',
|
||||
name: 'Smith, John A.',
|
||||
primaryMos: DutyMos('11B'),
|
||||
payGrade: AuthorizedRank.e5,
|
||||
readinessState: ReadinessState.fullyMissionCapable,
|
||||
);
|
||||
});
|
||||
|
||||
group('assignment status', () {
|
||||
test('unassigned soldier has no slot ID', () {
|
||||
expect(soldier.isUnassigned, isTrue);
|
||||
expect(soldier.isAssigned, isFalse);
|
||||
expect(soldier.assignedSlotId, isNull);
|
||||
});
|
||||
|
||||
test('assigned soldier has a slot ID', () {
|
||||
final assigned = soldier.copyWith(assignedSlotId: 'slot-001');
|
||||
expect(assigned.isAssigned, isTrue);
|
||||
expect(assigned.isUnassigned, isFalse);
|
||||
expect(assigned.assignedSlotId, equals('slot-001'));
|
||||
});
|
||||
});
|
||||
|
||||
group('readiness state', () {
|
||||
test('FMC soldier is fully mission capable', () {
|
||||
expect(soldier.isFmc, isTrue);
|
||||
});
|
||||
|
||||
test('PMC soldier is not fully mission capable', () {
|
||||
final pmc = soldier.copyWith(
|
||||
readinessState: ReadinessState.partiallyMissionCapable,
|
||||
);
|
||||
expect(pmc.isFmc, isFalse);
|
||||
});
|
||||
|
||||
test('NMC soldier is not fully mission capable', () {
|
||||
final nmc = soldier.copyWith(
|
||||
readinessState: ReadinessState.notMissionCapable,
|
||||
);
|
||||
expect(nmc.isFmc, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('copyWith', () {
|
||||
test('copies with updated name', () {
|
||||
final updated = soldier.copyWith(name: 'Jones, Mary B.');
|
||||
expect(updated.name, equals('Jones, Mary B.'));
|
||||
expect(updated.soldierId, equals(soldier.soldierId));
|
||||
});
|
||||
|
||||
test('copies with updated pay grade', () {
|
||||
final updated = soldier.copyWith(payGrade: AuthorizedRank.e6);
|
||||
expect(updated.payGrade, equals(AuthorizedRank.e6));
|
||||
});
|
||||
|
||||
test('clearAssignment removes slot assignment', () {
|
||||
final assigned = soldier.copyWith(assignedSlotId: 'slot-001');
|
||||
final cleared = assigned.copyWith(clearAssignment: true);
|
||||
expect(cleared.assignedSlotId, isNull);
|
||||
expect(cleared.isUnassigned, isTrue);
|
||||
});
|
||||
|
||||
test('does not modify original', () {
|
||||
soldier.copyWith(name: 'Modified');
|
||||
expect(soldier.name, equals('Smith, John A.'));
|
||||
});
|
||||
});
|
||||
|
||||
group('equality', () {
|
||||
test('two records with same fields are equal', () {
|
||||
final record1 = PersonnelRecord(
|
||||
soldierId: 'test-001',
|
||||
name: 'Test Soldier',
|
||||
primaryMos: DutyMos('11B'),
|
||||
payGrade: AuthorizedRank.e4,
|
||||
readinessState: ReadinessState.fullyMissionCapable,
|
||||
);
|
||||
final record2 = PersonnelRecord(
|
||||
soldierId: 'test-001',
|
||||
name: 'Test Soldier',
|
||||
primaryMos: DutyMos('11B'),
|
||||
payGrade: AuthorizedRank.e4,
|
||||
readinessState: ReadinessState.fullyMissionCapable,
|
||||
);
|
||||
expect(record1, equals(record2));
|
||||
});
|
||||
|
||||
test('two records with different IDs are not equal', () {
|
||||
final record1 = PersonnelRecord(
|
||||
soldierId: 'test-001',
|
||||
name: 'Test Soldier',
|
||||
primaryMos: DutyMos('11B'),
|
||||
payGrade: AuthorizedRank.e4,
|
||||
readinessState: ReadinessState.fullyMissionCapable,
|
||||
);
|
||||
final record2 = PersonnelRecord(
|
||||
soldierId: 'test-002',
|
||||
name: 'Test Soldier',
|
||||
primaryMos: DutyMos('11B'),
|
||||
payGrade: AuthorizedRank.e4,
|
||||
readinessState: ReadinessState.fullyMissionCapable,
|
||||
);
|
||||
expect(record1, isNot(equals(record2)));
|
||||
});
|
||||
});
|
||||
|
||||
group('ReadinessState', () {
|
||||
test('displayName returns correct abbreviations', () {
|
||||
expect(
|
||||
ReadinessState.fullyMissionCapable.displayName,
|
||||
equals('FMC'),
|
||||
);
|
||||
expect(
|
||||
ReadinessState.partiallyMissionCapable.displayName,
|
||||
equals('PMC'),
|
||||
);
|
||||
expect(
|
||||
ReadinessState.notMissionCapable.displayName,
|
||||
equals('NMC'),
|
||||
);
|
||||
expect(ReadinessState.unknown.displayName, equals('Unknown'));
|
||||
});
|
||||
});
|
||||
|
||||
group('toString', () {
|
||||
test('includes key fields in string representation', () {
|
||||
final str = soldier.toString();
|
||||
expect(str, contains('soldier-001'));
|
||||
expect(str, contains('Smith, John A.'));
|
||||
expect(str, contains('11B'));
|
||||
expect(str, contains('E-5'));
|
||||
expect(str, contains('FMC'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
|
||||
import 'package:uto_manager/domain/entities/echelon.dart';
|
||||
import 'package:uto_manager/domain/entities/unit_node.dart';
|
||||
|
||||
void main() {
|
||||
group('UnitNode', () {
|
||||
late UnitNode brigadeNode;
|
||||
late UnitNode battalionNode;
|
||||
late UnitNode companyNode;
|
||||
late UnitNode platoonNode;
|
||||
|
||||
setUp(() {
|
||||
brigadeNode = const UnitNode(
|
||||
unitId: 'brigade-001',
|
||||
echelon: Echelon.brigade,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: '1st Brigade Combat Team',
|
||||
isKeyLeader: false,
|
||||
);
|
||||
|
||||
battalionNode = const UnitNode(
|
||||
unitId: 'battalion-001',
|
||||
echelon: Echelon.battalion,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: '1-1 Infantry Battalion',
|
||||
isKeyLeader: false,
|
||||
parentUnitId: 'brigade-001',
|
||||
);
|
||||
|
||||
companyNode = const UnitNode(
|
||||
unitId: 'company-001',
|
||||
echelon: Echelon.company,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: 'Alpha Company',
|
||||
isKeyLeader: false,
|
||||
parentUnitId: 'battalion-001',
|
||||
);
|
||||
|
||||
platoonNode = const UnitNode(
|
||||
unitId: 'platoon-001',
|
||||
echelon: Echelon.platoon,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: '1st Platoon',
|
||||
isKeyLeader: false,
|
||||
parentUnitId: 'company-001',
|
||||
);
|
||||
});
|
||||
|
||||
group('isRoot', () {
|
||||
test('brigade with no parent is a root node', () {
|
||||
expect(brigadeNode.isRoot, isTrue);
|
||||
});
|
||||
|
||||
test('battalion with a parent is not a root node', () {
|
||||
expect(battalionNode.isRoot, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('isValidParentNode', () {
|
||||
test('brigade is a valid parent for battalion', () {
|
||||
expect(battalionNode.isValidParentNode(brigadeNode), isTrue);
|
||||
});
|
||||
|
||||
test('battalion is a valid parent for company', () {
|
||||
expect(companyNode.isValidParentNode(battalionNode), isTrue);
|
||||
});
|
||||
|
||||
test('company is a valid parent for platoon', () {
|
||||
expect(platoonNode.isValidParentNode(companyNode), isTrue);
|
||||
});
|
||||
|
||||
test('brigade is NOT a valid parent for company (skips echelon)', () {
|
||||
expect(companyNode.isValidParentNode(brigadeNode), isFalse);
|
||||
});
|
||||
|
||||
test('platoon is NOT a valid parent for battalion', () {
|
||||
expect(battalionNode.isValidParentNode(platoonNode), isFalse);
|
||||
});
|
||||
|
||||
test('same echelon is NOT a valid parent', () {
|
||||
const anotherBrigade = UnitNode(
|
||||
unitId: 'brigade-002',
|
||||
echelon: Echelon.brigade,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: '2nd Brigade',
|
||||
isKeyLeader: false,
|
||||
);
|
||||
expect(brigadeNode.isValidParentNode(anotherBrigade), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('copyWith', () {
|
||||
test('copies with updated name', () {
|
||||
final updated = brigadeNode.copyWith(name: 'Updated Brigade');
|
||||
expect(updated.name, equals('Updated Brigade'));
|
||||
expect(updated.unitId, equals(brigadeNode.unitId));
|
||||
expect(updated.echelon, equals(brigadeNode.echelon));
|
||||
});
|
||||
|
||||
test('copies with updated parentUnitId', () {
|
||||
final updated = brigadeNode.copyWith(parentUnitId: 'new-parent-id');
|
||||
expect(updated.parentUnitId, equals('new-parent-id'));
|
||||
});
|
||||
|
||||
test('clearParent removes parentUnitId', () {
|
||||
final updated = battalionNode.copyWith(clearParent: true);
|
||||
expect(updated.parentUnitId, isNull);
|
||||
expect(updated.isRoot, isTrue);
|
||||
});
|
||||
|
||||
test('does not modify original', () {
|
||||
brigadeNode.copyWith(name: 'Modified');
|
||||
expect(brigadeNode.name, equals('1st Brigade Combat Team'));
|
||||
});
|
||||
});
|
||||
|
||||
group('equality', () {
|
||||
test('two nodes with same fields are equal', () {
|
||||
const node1 = UnitNode(
|
||||
unitId: 'test-001',
|
||||
echelon: Echelon.platoon,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: 'Test Platoon',
|
||||
isKeyLeader: false,
|
||||
);
|
||||
const node2 = UnitNode(
|
||||
unitId: 'test-001',
|
||||
echelon: Echelon.platoon,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: 'Test Platoon',
|
||||
isKeyLeader: false,
|
||||
);
|
||||
expect(node1, equals(node2));
|
||||
});
|
||||
|
||||
test('two nodes with different IDs are not equal', () {
|
||||
const node1 = UnitNode(
|
||||
unitId: 'test-001',
|
||||
echelon: Echelon.platoon,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: 'Test Platoon',
|
||||
isKeyLeader: false,
|
||||
);
|
||||
const node2 = UnitNode(
|
||||
unitId: 'test-002',
|
||||
echelon: Echelon.platoon,
|
||||
branch: DoctrinalBranch.infantry,
|
||||
name: 'Test Platoon',
|
||||
isKeyLeader: false,
|
||||
);
|
||||
expect(node1, isNot(equals(node2)));
|
||||
});
|
||||
});
|
||||
|
||||
group('toString', () {
|
||||
test('includes key fields in string representation', () {
|
||||
final str = brigadeNode.toString();
|
||||
expect(str, contains('brigade-001'));
|
||||
expect(str, contains('1st Brigade Combat Team'));
|
||||
expect(str, contains('Brigade'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
|
||||
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
||||
|
||||
void main() {
|
||||
group('DutyMos', () {
|
||||
group('constructor validation', () {
|
||||
test('accepts valid known MOS codes', () {
|
||||
expect(() => DutyMos('11B'), returnsNormally);
|
||||
expect(() => DutyMos('68W'), returnsNormally);
|
||||
expect(() => DutyMos('13F'), returnsNormally);
|
||||
expect(() => DutyMos('25H'), returnsNormally);
|
||||
expect(() => DutyMos('17E'), returnsNormally);
|
||||
expect(() => DutyMos('92Y'), returnsNormally);
|
||||
expect(() => DutyMos('12B'), returnsNormally);
|
||||
});
|
||||
|
||||
test('normalizes lowercase input to uppercase', () {
|
||||
final mos = DutyMos('11b');
|
||||
expect(mos.code, equals('11B'));
|
||||
});
|
||||
|
||||
test('trims whitespace from input', () {
|
||||
final mos = DutyMos(' 11B ');
|
||||
expect(mos.code, equals('11B'));
|
||||
});
|
||||
|
||||
test('throws ArgumentError for invalid format — too short', () {
|
||||
expect(() => DutyMos('1B'), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('throws ArgumentError for invalid format — too long', () {
|
||||
expect(() => DutyMos('11BB'), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('throws ArgumentError for invalid format — all digits', () {
|
||||
expect(() => DutyMos('111'), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('throws ArgumentError for invalid format — all letters', () {
|
||||
expect(() => DutyMos('AAA'), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('throws ArgumentError for unknown but validly-formatted MOS', () {
|
||||
expect(() => DutyMos('99Z'), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('throws ArgumentError for empty string', () {
|
||||
expect(() => DutyMos(''), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
group('unvalidated constructor', () {
|
||||
test('accepts valid format without dictionary check', () {
|
||||
expect(() => DutyMos.unvalidated('99Z'), returnsNormally);
|
||||
});
|
||||
|
||||
test('still rejects invalid format', () {
|
||||
expect(() => DutyMos.unvalidated('1B'), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
group('isKnown', () {
|
||||
test('returns true for known MOS codes', () {
|
||||
expect(DutyMos.isKnown('11B'), isTrue);
|
||||
expect(DutyMos.isKnown('68W'), isTrue);
|
||||
expect(DutyMos.isKnown('13F'), isTrue);
|
||||
expect(DutyMos.isKnown('25H'), isTrue);
|
||||
expect(DutyMos.isKnown('17E'), isTrue);
|
||||
expect(DutyMos.isKnown('92Y'), isTrue);
|
||||
expect(DutyMos.isKnown('12B'), isTrue);
|
||||
expect(DutyMos.isKnown('91B'), isTrue);
|
||||
expect(DutyMos.isKnown('15W'), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for unknown MOS codes', () {
|
||||
expect(DutyMos.isKnown('99Z'), isFalse);
|
||||
expect(DutyMos.isKnown('00A'), isFalse);
|
||||
expect(DutyMos.isKnown('XYZ'), isFalse);
|
||||
});
|
||||
|
||||
test('is case-insensitive', () {
|
||||
expect(DutyMos.isKnown('11b'), isTrue);
|
||||
expect(DutyMos.isKnown('68w'), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('codesForBranch', () {
|
||||
test('returns infantry MOS codes', () {
|
||||
final codes = DutyMos.codesForBranch(DoctrinalBranch.infantry);
|
||||
expect(codes, contains('11B'));
|
||||
expect(codes, contains('11A'));
|
||||
expect(codes, contains('15W'));
|
||||
});
|
||||
|
||||
test('returns medical MOS codes', () {
|
||||
final codes = DutyMos.codesForBranch(DoctrinalBranch.medical);
|
||||
expect(codes, contains('68W'));
|
||||
expect(codes, isNot(contains('11B')));
|
||||
});
|
||||
|
||||
test('returns fires MOS codes', () {
|
||||
final codes = DutyMos.codesForBranch(DoctrinalBranch.fires);
|
||||
expect(codes, contains('13F'));
|
||||
expect(codes, contains('13D'));
|
||||
expect(codes, isNot(contains('68W')));
|
||||
});
|
||||
|
||||
test('returns signal MOS codes', () {
|
||||
final codes = DutyMos.codesForBranch(DoctrinalBranch.signal);
|
||||
expect(codes, contains('25H'));
|
||||
expect(codes, contains('25B'));
|
||||
});
|
||||
|
||||
test('returns cyberEw MOS codes', () {
|
||||
final codes = DutyMos.codesForBranch(DoctrinalBranch.cyberEw);
|
||||
expect(codes, contains('17E'));
|
||||
expect(codes, contains('17C'));
|
||||
});
|
||||
|
||||
test('returns logistics MOS codes', () {
|
||||
final codes = DutyMos.codesForBranch(DoctrinalBranch.logistics);
|
||||
expect(codes, contains('92Y'));
|
||||
expect(codes, contains('92A'));
|
||||
expect(codes, contains('91B'));
|
||||
});
|
||||
|
||||
test('returns engineer MOS codes', () {
|
||||
final codes = DutyMos.codesForBranch(DoctrinalBranch.engineer);
|
||||
expect(codes, contains('12B'));
|
||||
expect(codes, contains('12A'));
|
||||
});
|
||||
});
|
||||
|
||||
group('equality', () {
|
||||
test('two DutyMos with same code are equal', () {
|
||||
final mos1 = DutyMos('11B');
|
||||
final mos2 = DutyMos('11B');
|
||||
expect(mos1, equals(mos2));
|
||||
});
|
||||
|
||||
test('two DutyMos with different codes are not equal', () {
|
||||
final mos1 = DutyMos('11B');
|
||||
final mos2 = DutyMos('68W');
|
||||
expect(mos1, isNot(equals(mos2)));
|
||||
});
|
||||
|
||||
test('hashCode is consistent with equality', () {
|
||||
final mos1 = DutyMos('11B');
|
||||
final mos2 = DutyMos('11B');
|
||||
expect(mos1.hashCode, equals(mos2.hashCode));
|
||||
});
|
||||
});
|
||||
|
||||
group('toString', () {
|
||||
test('returns the MOS code string', () {
|
||||
expect(DutyMos('11B').toString(), equals('11B'));
|
||||
expect(DutyMos('68W').toString(), equals('68W'));
|
||||
});
|
||||
});
|
||||
|
||||
group('all defined specialties are accepted', () {
|
||||
// Infantry & Maneuver
|
||||
test('accepts all infantry MOS codes', () {
|
||||
for (final code in ['11A', '11B', '11C', '15W', '15A', '15T']) {
|
||||
expect(
|
||||
() => DutyMos(code),
|
||||
returnsNormally,
|
||||
reason: '$code should be a valid MOS',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Medical
|
||||
test('accepts all medical MOS codes', () {
|
||||
for (final code in ['68W', '68A', '68D', '68J', '68K']) {
|
||||
expect(
|
||||
() => DutyMos(code),
|
||||
returnsNormally,
|
||||
reason: '$code should be a valid MOS',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Field Artillery
|
||||
test('accepts all fires MOS codes', () {
|
||||
for (final code in ['13A', '13B', '13D', '13F', '13J', '13M']) {
|
||||
expect(
|
||||
() => DutyMos(code),
|
||||
returnsNormally,
|
||||
reason: '$code should be a valid MOS',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Signal
|
||||
test('accepts all signal MOS codes', () {
|
||||
for (final code in ['25A', '25B', '25H', '25U', '25N']) {
|
||||
expect(
|
||||
() => DutyMos(code),
|
||||
returnsNormally,
|
||||
reason: '$code should be a valid MOS',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cyber/EW
|
||||
test('accepts all cyber/EW MOS codes', () {
|
||||
for (final code in ['17A', '17C', '17E', '17S']) {
|
||||
expect(
|
||||
() => DutyMos(code),
|
||||
returnsNormally,
|
||||
reason: '$code should be a valid MOS',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Logistics
|
||||
test('accepts all logistics MOS codes', () {
|
||||
for (final code in ['91B', '91A', '92A', '92Y', '92F']) {
|
||||
expect(
|
||||
() => DutyMos(code),
|
||||
returnsNormally,
|
||||
reason: '$code should be a valid MOS',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Engineer
|
||||
test('accepts all engineer MOS codes', () {
|
||||
for (final code in ['12A', '12B', '12C', '12N', '12Z']) {
|
||||
expect(
|
||||
() => DutyMos(code),
|
||||
returnsNormally,
|
||||
reason: '$code should be a valid MOS',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:uto_manager/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('UTO Manager placeholder scaffold renders', (tester) async {
|
||||
await tester.pumpWidget(const UtoManagerApp());
|
||||
|
||||
expect(find.text('UTO Manager'), findsOneWidget);
|
||||
expect(find.text('Unit Task Organization Manager'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.military_tech), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Flutter Plugins" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
|
After Width: | Height: | Size: 917 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="uto_manager">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>uto_manager</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
You can customize the "flutter_bootstrap.js" script.
|
||||
This is useful to provide a custom configuration to the Flutter loader
|
||||
or to give the user feedback during the initialization process.
|
||||
|
||||
For more details:
|
||||
* https://docs.flutter.dev/platform-integration/web/initialization
|
||||
-->
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "uto_manager",
|
||||
"short_name": "uto_manager",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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/
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(uto_manager 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 "uto_manager")
|
||||
|
||||
# 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 "$<$<CONFIG:Debug>:_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 "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||
# 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)
|
||||
|
|
@ -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} $<CONFIG>
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.utomanager" "\0"
|
||||
VALUE "FileDescription", "uto_manager" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "uto_manager" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2026 com.utomanager. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "uto_manager.exe" "\0"
|
||||
VALUE "ProductName", "uto_manager" "\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
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#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<flutter::FlutterViewController>(
|
||||
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<LRESULT> 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);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||
#define RUNNER_FLUTTER_WINDOW_H_
|
||||
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#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::FlutterViewController> flutter_controller_;
|
||||
};
|
||||
|
||||
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
|
||||
#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<std::string> 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"uto_manager", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
::CoUninitialize();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
#include "utils.h"
|
||||
|
||||
#include <flutter_windows.h>
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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<std::string> 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::string>();
|
||||
}
|
||||
|
||||
std::vector<std::string> 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<int>(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<size_t>(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;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#ifndef RUNNER_UTILS_H_
|
||||
#define RUNNER_UTILS_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 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<std::string>,
|
||||
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||
std::vector<std::string> GetCommandLineArguments();
|
||||
|
||||
#endif // RUNNER_UTILS_H_
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
#include "win32_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#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<int>(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<EnableNonClientDpiScaling*>(
|
||||
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<LONG>(origin.x),
|
||||
static_cast<LONG>(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<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(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<RECT*>(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<Win32Window*>(
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||
#define RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// 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_
|
||||