remove experimental ground-suite project
Signed-off-by: Onur Özkan <work@onurozkan.dev>
No files matched your search
@@ -9,7 +9,6 @@
|
|||||||
- [Configuration](configuration.md)
|
- [Configuration](configuration.md)
|
||||||
- [Logs](logs.md)
|
- [Logs](logs.md)
|
||||||
- [Errors](errors.md)
|
- [Errors](errors.md)
|
||||||
- [Ground Suite](ground-suite.md)
|
|
||||||
- [Boards](boards/index.md)
|
- [Boards](boards/index.md)
|
||||||
- [Kakute F7 Mini V3](boards/kakute-f7-mini-v3.md)
|
- [Kakute F7 Mini V3](boards/kakute-f7-mini-v3.md)
|
||||||
- [GEPRC GEP-F722-HD V2](boards/geprc-f722-v2.md)
|
- [GEPRC GEP-F722-HD V2](boards/geprc-f722-v2.md)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ target-specific commands in [Development and verification](development.md).
|
|||||||
| `firmware/platforms/stm32f7/` | Reviewed F722 RTIC platform and peripheral ownership. |
|
| `firmware/platforms/stm32f7/` | Reviewed F722 RTIC platform and peripheral ownership. |
|
||||||
| `firmware/libs/` | Typed math, estimation, control, safety, protocol, and driver crates. |
|
| `firmware/libs/` | Typed math, estimation, control, safety, protocol, and driver crates. |
|
||||||
| `firmware/tools/sitl/` | Host-only closed-loop Gazebo simulation. |
|
| `firmware/tools/sitl/` | Host-only closed-loop Gazebo simulation. |
|
||||||
| `ground-suite/` | Desktop ground-control application. |
|
|
||||||
| `docs/` | This Markdown documentation site. |
|
| `docs/` | This Markdown documentation site. |
|
||||||
|
|
||||||
Do not add board-local READMEs. Add a page here instead, then link it from
|
Do not add board-local READMEs. Add a page here instead, then link it from
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# Ground Suite
|
|
||||||
|
|
||||||
`ground-suite/` is Kaptan's desktop Flutter application. Its primary
|
|
||||||
workspace is the Tactical Command surface: a spatial local scene, local units,
|
|
||||||
and session-local drawings and markers. The decoded diagnostic event stream is
|
|
||||||
always reachable from the tactical header.
|
|
||||||
|
|
||||||
## Application model
|
|
||||||
|
|
||||||
The core model keeps these concepts independent:
|
|
||||||
|
|
||||||
- **Vehicle definition** — immutable reviewed profile, catalog identity, and
|
|
||||||
bundled presentation media.
|
|
||||||
- **Local fleet unit** — an operator-created instance of a vehicle definition.
|
|
||||||
Local-unit IDs and in-memory planning placements belong to the workspace,
|
|
||||||
not the vehicle definition. Current placement is local presentation data and
|
|
||||||
is not yet persisted.
|
|
||||||
- **Received vehicle data** — transport input and diagnostic events. It cannot
|
|
||||||
create, position, identify, or authorize a local fleet unit.
|
|
||||||
|
|
||||||
This separation gives the fleet interface a stable path from local planning to
|
|
||||||
future live operation without coupling visual selection to incoming telemetry.
|
|
||||||
|
|
||||||
## Tactical workspace
|
|
||||||
|
|
||||||
The default route is Tactical Command. Its map-edge `UNITS`, `CURSOR`, and
|
|
||||||
`VIEW` groups are local interactions, not MAVLink controls. `Selection cursor`
|
|
||||||
is the default mode for map and rectangle selection. `Pencil cursor` creates
|
|
||||||
session-local freehand strokes; its top toolbar controls hue, saturation,
|
|
||||||
brightness, and thickness. `Marker cursor` has an independent full-color
|
|
||||||
style and creates a named local marker only after a map click and explicit
|
|
||||||
save. The shared disclosure keeps local scene data,
|
|
||||||
vehicle link state, and command authority separate. Local drawings and markers
|
|
||||||
are never reported vehicle positions.
|
|
||||||
|
|
||||||
`Add local unit` opens the horizontal local hangar. Choosing a definition only
|
|
||||||
prepares a local unit draft; the explicit setup confirmation creates and
|
|
||||||
selects a locally scattered scene unit, without asking the operator to click
|
|
||||||
the map. Local setup currently permits only the workspace label. Vehicle
|
|
||||||
configuration remains unavailable until verified binding, compatible schema,
|
|
||||||
command authority, and safe activation are available. This presentation
|
|
||||||
placement is temporary until reviewed MAVLink position data can drive a
|
|
||||||
distinct reported-position layer. The right inspector and bottom selection
|
|
||||||
console are available outside the map for keyboard and assistive-technology
|
|
||||||
access. These interactions create only session-local workspace data.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```text
|
|
||||||
VehicleDataPort -> typed observation updates -> observability state -> event stream
|
|
||||||
Vehicle catalog -> local-unit workspace state -> tactical scene and unit inspector
|
|
||||||
```
|
|
||||||
|
|
||||||
The transport path and local-fleet path intentionally meet only through future
|
|
||||||
explicit bindings. A future MAVLink adapter combines pinned MAVLink Common and
|
|
||||||
Kaptan dialect definitions at the decoding boundary.
|
|
||||||
|
|
||||||
## Follow-up work
|
|
||||||
|
|
||||||
- TODO(gcs-fleet-storage): versioned persistence for local units and catalog
|
|
||||||
identities.
|
|
||||||
- TODO(gcs-source-binding): explicit source-to-unit binding.
|
|
||||||
- TODO(gcs-map-pose): coordinate datum, timestamped pose, uncertainty, and
|
|
||||||
staleness handling for physical map placement.
|
|
||||||
- TODO(gcs-map-renderer): owned Linux GPU map host, pinned MapLibre assets,
|
|
||||||
and reviewed vector/DEM data source.
|
|
||||||
- TODO(gcs-platform-match): compare reported build/schema metadata to the
|
|
||||||
local definition.
|
|
||||||
- TODO(gcs-command-surface): add command interaction only with its reviewed
|
|
||||||
authority, target-binding, and safety contracts.
|
|
||||||
- TODO(gcs-catalog): generate installed definitions and category metadata from
|
|
||||||
reviewed firmware build manifests.
|
|
||||||
@@ -12,8 +12,7 @@ Markdown.
|
|||||||
## Index
|
## Index
|
||||||
|
|
||||||
- New contributors should read [Getting started](getting-started.md).
|
- New contributors should read [Getting started](getting-started.md).
|
||||||
- Firmware source and its Rust workspace begin in `firmware/`; the desktop
|
- Firmware source and its Rust workspace begin in `firmware/`.
|
||||||
application is a sibling project in `ground-suite/`.
|
|
||||||
- Board work belongs under [Boards](boards/index.md).
|
- Board work belongs under [Boards](boards/index.md).
|
||||||
- The currently supported closed-loop simulation is [Host SITL](sitl.md).
|
- The currently supported closed-loop simulation is [Host SITL](sitl.md).
|
||||||
- [Configuration](configuration.md) distinguishes the current SITL profile
|
- [Configuration](configuration.md) distinguishes the current SITL profile
|
||||||
|
|||||||
@@ -20,7 +20,3 @@ Run Cargo commands from this directory:
|
|||||||
cd firmware
|
cd firmware
|
||||||
cargo test --workspace --locked --offline
|
cargo test --workspace --locked --offline
|
||||||
```
|
```
|
||||||
|
|
||||||
The sibling `ground-suite/` directory is the desktop ground-control
|
|
||||||
application. It is intentionally a separate project and does not participate
|
|
||||||
in the firmware Cargo workspace.
|
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
pkgs.rustup
|
pkgs.rustup
|
||||||
pkgs.dfu-util
|
pkgs.dfu-util
|
||||||
pkgs.mdbook
|
pkgs.mdbook
|
||||||
pkgs.flutter
|
|
||||||
python
|
python
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# Miscellaneous
|
|
||||||
*.class
|
|
||||||
*.log
|
|
||||||
*.pyc
|
|
||||||
*.swp
|
|
||||||
.DS_Store
|
|
||||||
.atom/
|
|
||||||
.build/
|
|
||||||
.buildlog/
|
|
||||||
.history
|
|
||||||
.svn/
|
|
||||||
.swiftpm/
|
|
||||||
migrate_working_dir/
|
|
||||||
|
|
||||||
# IntelliJ related
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
|
||||||
# is commented out by default.
|
|
||||||
#.vscode/
|
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
|
||||||
**/doc/api/
|
|
||||||
**/ios/Flutter/.last_build_id
|
|
||||||
.dart_tool/
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
.pub-cache/
|
|
||||||
.pub/
|
|
||||||
/build/
|
|
||||||
/coverage/
|
|
||||||
|
|
||||||
# Symbolication related
|
|
||||||
app.*.symbols
|
|
||||||
|
|
||||||
# Obfuscation related
|
|
||||||
app.*.map.json
|
|
||||||
|
|
||||||
# Android Studio will place build artifacts here
|
|
||||||
/android/app/debug
|
|
||||||
/android/app/profile
|
|
||||||
/android/app/release
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# 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: "nixpkgs000000000000000000000000000000000"
|
|
||||||
channel: "stable"
|
|
||||||
|
|
||||||
project_type: app
|
|
||||||
|
|
||||||
# Tracks metadata for the flutter migrate command
|
|
||||||
migration:
|
|
||||||
platforms:
|
|
||||||
- platform: root
|
|
||||||
create_revision: nixpkgs000000000000000000000000000000000
|
|
||||||
base_revision: nixpkgs000000000000000000000000000000000
|
|
||||||
- platform: linux
|
|
||||||
create_revision: nixpkgs000000000000000000000000000000000
|
|
||||||
base_revision: nixpkgs000000000000000000000000000000000
|
|
||||||
|
|
||||||
# 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'
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Ground Suite
|
|
||||||
|
|
||||||
Ground Suite is Kaptan's desktop ground-control application.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
nix develop -c flutter analyze ground-suite
|
|
||||||
nix develop -c dart format --set-exit-if-changed ground-suite
|
|
||||||
nix develop -c sh -c 'cd ground-suite && flutter test'
|
|
||||||
nix develop -c sh -c 'cd ground-suite && flutter run -d linux'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
- `lib/app/` — application shell and appearance.
|
|
||||||
- `lib/features/fleet/` — fleet canvas and local-unit workspace.
|
|
||||||
- `lib/features/vehicle_workspace/` — installed vehicle definitions and Add-unit flow.
|
|
||||||
- `lib/features/diagnostics/` — diagnostic event stream.
|
|
||||||
- `lib/core/` — transport and MAVLink decoding boundaries.
|
|
||||||
|
|
||||||
The durable design and integration notes live in [the documentation](../docs/src/ground-suite.md).
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
|
||||||
# check for errors, warnings, and lints.
|
|
||||||
#
|
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
|
||||||
|
|
||||||
linter:
|
|
||||||
# The lint rules applied to this project can be customized in the
|
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
||||||
# included above or to enable additional rules. A list of all available lints
|
|
||||||
# and their documentation is published at https://dart.dev/lints.
|
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
|
Before Width: | Height: | Size: 2.7 MiB |
@@ -1,4 +0,0 @@
|
|||||||
# Asset notice
|
|
||||||
|
|
||||||
Assets in this directory are project-supplied. Replacements require reviewed
|
|
||||||
ownership, provenance, and licensing.
|
|
||||||
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -1,20 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import 'app_shell.dart';
|
|
||||||
import 'theme.dart';
|
|
||||||
|
|
||||||
class GroundSuiteApp extends StatelessWidget {
|
|
||||||
const GroundSuiteApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => ProviderScope(
|
|
||||||
child: MaterialApp(
|
|
||||||
title: 'Ground Suite',
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
theme: groundSuiteDarkTheme(),
|
|
||||||
themeMode: ThemeMode.dark,
|
|
||||||
home: const AppShell(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import '../features/command/presentation/tactical_command_page.dart';
|
|
||||||
import '../features/diagnostics/presentation/diagnostics_page.dart';
|
|
||||||
import '../features/observability/presentation/observability_controller.dart';
|
|
||||||
|
|
||||||
enum _ApplicationSurface { tactical, eventStream }
|
|
||||||
|
|
||||||
/// Owns the two first-class Ground Suite surfaces. Tactical planning never
|
|
||||||
/// hides decoded diagnostics or critical accessibility announcements.
|
|
||||||
class AppShell extends ConsumerStatefulWidget {
|
|
||||||
const AppShell({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<AppShell> createState() => _AppShellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppShellState extends ConsumerState<AppShell> {
|
|
||||||
_ApplicationSurface _surface = _ApplicationSurface.tactical;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final observation = ref.watch(observabilityControllerProvider);
|
|
||||||
return Scaffold(
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
switch (_surface) {
|
|
||||||
_ApplicationSurface.tactical => TacticalCommandPage(
|
|
||||||
sourceLabel: observation.sourceLabel,
|
|
||||||
sourceStatus: observation.status.label,
|
|
||||||
onOpenEventStream: () =>
|
|
||||||
setState(() => _surface = _ApplicationSurface.eventStream),
|
|
||||||
),
|
|
||||||
_ApplicationSurface.eventStream => _EventStreamWorkspace(
|
|
||||||
onReturnToTactical: () =>
|
|
||||||
setState(() => _surface = _ApplicationSurface.tactical),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Semantics(
|
|
||||||
liveRegion: true,
|
|
||||||
label: observation.accessibilityAnnouncement ?? '',
|
|
||||||
child: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EventStreamWorkspace extends StatelessWidget {
|
|
||||||
const _EventStreamWorkspace({required this.onReturnToTactical});
|
|
||||||
|
|
||||||
final VoidCallback onReturnToTactical;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Stack(
|
|
||||||
children: [
|
|
||||||
const Positioned.fill(child: DiagnosticsPage()),
|
|
||||||
Positioned(
|
|
||||||
top: 16,
|
|
||||||
right: 20,
|
|
||||||
child: FilledButton.icon(
|
|
||||||
onPressed: onReturnToTactical,
|
|
||||||
icon: const Icon(Icons.map_outlined),
|
|
||||||
label: const Text('Tactical workspace'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
|
|
||||||
/// Ground Suite's single visual language: a dark tactical operations deck.
|
|
||||||
const panelBackground = Color(0xD912141A);
|
|
||||||
const panelBorder = Color(0x1AFFFFFF);
|
|
||||||
const textPrimary = Color(0xFFF8FAFC);
|
|
||||||
const textSecondary = Color(0xFF94A3B8);
|
|
||||||
const accentOrange = Color(0xFFF97316);
|
|
||||||
const accentMint = Color(0xFF34D399);
|
|
||||||
const statusGreen = Color(0xFF10B981);
|
|
||||||
const statusRed = Color(0xFFEF4444);
|
|
||||||
|
|
||||||
const tacticalBlack = Color(0xFF000000);
|
|
||||||
const tacticalTransparent = Color(0x00000000);
|
|
||||||
|
|
||||||
ThemeData groundSuiteDarkTheme() {
|
|
||||||
final base = ThemeData.dark(useMaterial3: true);
|
|
||||||
final interTextTheme = GoogleFonts.interTextTheme(
|
|
||||||
base.textTheme,
|
|
||||||
).apply(bodyColor: textPrimary, displayColor: textPrimary);
|
|
||||||
|
|
||||||
return base.copyWith(
|
|
||||||
scaffoldBackgroundColor: tacticalBlack,
|
|
||||||
cardColor: panelBackground,
|
|
||||||
colorScheme: const ColorScheme.dark(
|
|
||||||
primary: accentMint,
|
|
||||||
onPrimary: tacticalBlack,
|
|
||||||
secondary: accentOrange,
|
|
||||||
onSecondary: tacticalBlack,
|
|
||||||
surface: panelBackground,
|
|
||||||
onSurface: textPrimary,
|
|
||||||
error: statusRed,
|
|
||||||
onError: textPrimary,
|
|
||||||
outline: panelBorder,
|
|
||||||
),
|
|
||||||
textTheme: interTextTheme.copyWith(
|
|
||||||
displayLarge: interTextTheme.displayLarge?.copyWith(color: textPrimary),
|
|
||||||
displayMedium: interTextTheme.displayMedium?.copyWith(color: textPrimary),
|
|
||||||
headlineLarge: interTextTheme.headlineLarge?.copyWith(color: textPrimary),
|
|
||||||
headlineMedium: interTextTheme.headlineMedium?.copyWith(
|
|
||||||
color: textPrimary,
|
|
||||||
),
|
|
||||||
headlineSmall: interTextTheme.headlineSmall?.copyWith(color: textPrimary),
|
|
||||||
titleLarge: interTextTheme.titleLarge?.copyWith(color: textPrimary),
|
|
||||||
titleMedium: interTextTheme.titleMedium?.copyWith(color: textPrimary),
|
|
||||||
titleSmall: interTextTheme.titleSmall?.copyWith(color: textPrimary),
|
|
||||||
bodyLarge: interTextTheme.bodyLarge?.copyWith(color: textPrimary),
|
|
||||||
bodyMedium: interTextTheme.bodyMedium?.copyWith(color: textPrimary),
|
|
||||||
bodySmall: interTextTheme.bodySmall?.copyWith(color: textSecondary),
|
|
||||||
labelLarge: interTextTheme.labelLarge?.copyWith(color: textPrimary),
|
|
||||||
labelMedium: interTextTheme.labelMedium?.copyWith(color: textSecondary),
|
|
||||||
labelSmall: interTextTheme.labelSmall?.copyWith(color: textSecondary),
|
|
||||||
),
|
|
||||||
cardTheme: const CardThemeData(color: panelBackground, elevation: 0),
|
|
||||||
dividerTheme: const DividerThemeData(color: panelBorder),
|
|
||||||
progressIndicatorTheme: ProgressIndicatorThemeData(
|
|
||||||
color: accentMint,
|
|
||||||
linearTrackColor: textSecondary.withValues(alpha: .2),
|
|
||||||
),
|
|
||||||
chipTheme: ChipThemeData(
|
|
||||||
backgroundColor: tacticalTransparent,
|
|
||||||
selectedColor: accentMint.withValues(alpha: .1),
|
|
||||||
side: const BorderSide(color: panelBorder),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
labelStyle: GoogleFonts.inter(color: textSecondary),
|
|
||||||
secondaryLabelStyle: GoogleFonts.inter(color: textPrimary),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
/// Receive-only bytes from one MAVLink transport endpoint.
|
|
||||||
///
|
|
||||||
/// A source has one reader and owns its close lifecycle. A future adapter will
|
|
||||||
/// bind serial, UDP, or a recording source here; no source in the first slice
|
|
||||||
/// may write bytes to a vehicle.
|
|
||||||
abstract interface class MavlinkByteSource {
|
|
||||||
Stream<Uint8List> get chunks;
|
|
||||||
|
|
||||||
Future<void> close();
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import '../observations/vehicle_data_update.dart';
|
|
||||||
import 'mavlink_byte_source.dart';
|
|
||||||
|
|
||||||
/// Converts a receive-only MAVLink byte stream into semantically safe events.
|
|
||||||
///
|
|
||||||
/// TODO(gcs-live-mavlink): generate this adapter from pinned MAVLink Common
|
|
||||||
/// definitions plus Kaptan's `kaptan.xml` root. It must reject MAVLink 1 and
|
|
||||||
/// CRC-invalid MAVLink 2 frames, validate every fixed diagnostic length before
|
|
||||||
/// UTF-8 decoding, retain unknown enum values and flags, and emit parser or
|
|
||||||
/// semantic drop updates without ending the connection.
|
|
||||||
abstract interface class MavlinkEventDecoder {
|
|
||||||
Stream<VehicleDataUpdate> decode(MavlinkByteSource source);
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
/// A MAVLink severity value retained exactly as it appeared on the wire.
|
|
||||||
///
|
|
||||||
/// Known values follow MAVLink's numeric severity semantics. Unknown values
|
|
||||||
/// remain displayable so a newer vehicle cannot silently lose diagnostic
|
|
||||||
/// meaning before Ground Suite has been updated.
|
|
||||||
final class MavSeverity {
|
|
||||||
const MavSeverity(this.wireValue);
|
|
||||||
|
|
||||||
static const emergency = MavSeverity(0);
|
|
||||||
static const alert = MavSeverity(1);
|
|
||||||
static const critical = MavSeverity(2);
|
|
||||||
static const error = MavSeverity(3);
|
|
||||||
static const warning = MavSeverity(4);
|
|
||||||
static const notice = MavSeverity(5);
|
|
||||||
static const info = MavSeverity(6);
|
|
||||||
static const debug = MavSeverity(7);
|
|
||||||
|
|
||||||
final int wireValue;
|
|
||||||
|
|
||||||
String get label => switch (wireValue) {
|
|
||||||
0 => 'Emergency',
|
|
||||||
1 => 'Alert',
|
|
||||||
2 => 'Critical',
|
|
||||||
3 => 'Error',
|
|
||||||
4 => 'Warning',
|
|
||||||
5 => 'Notice',
|
|
||||||
6 => 'Info',
|
|
||||||
7 => 'Debug',
|
|
||||||
_ => 'Unknown ($wireValue)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A KAPTAN_SHUTDOWN reason retained exactly as it appeared on the wire.
|
|
||||||
final class KaptanShutdownReason {
|
|
||||||
const KaptanShutdownReason(this.wireValue);
|
|
||||||
|
|
||||||
static const operator = KaptanShutdownReason(1);
|
|
||||||
static const safety = KaptanShutdownReason(2);
|
|
||||||
|
|
||||||
final int wireValue;
|
|
||||||
|
|
||||||
String get label => switch (wireValue) {
|
|
||||||
1 => 'Operator',
|
|
||||||
2 => 'Safety',
|
|
||||||
_ => 'Unknown ($wireValue)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TelemetryKind {
|
|
||||||
rawImu,
|
|
||||||
distanceSensor,
|
|
||||||
opticalFlow;
|
|
||||||
|
|
||||||
String get label => switch (this) {
|
|
||||||
TelemetryKind.rawImu => 'Raw IMU',
|
|
||||||
TelemetryKind.distanceSensor => 'Distance sensor',
|
|
||||||
TelemetryKind.opticalFlow => 'Optical flow',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ObservedEvent {
|
|
||||||
const ObservedEvent({required this.receivedAt});
|
|
||||||
|
|
||||||
final DateTime receivedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class HeartbeatEvent extends ObservedEvent {
|
|
||||||
const HeartbeatEvent({
|
|
||||||
required super.receivedAt,
|
|
||||||
required this.systemId,
|
|
||||||
required this.componentId,
|
|
||||||
required this.armed,
|
|
||||||
required this.vehicleState,
|
|
||||||
});
|
|
||||||
|
|
||||||
final int systemId;
|
|
||||||
final int componentId;
|
|
||||||
final bool armed;
|
|
||||||
final String vehicleState;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class TelemetryEvent extends ObservedEvent {
|
|
||||||
const TelemetryEvent({required super.receivedAt, required this.kind});
|
|
||||||
|
|
||||||
final TelemetryKind kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class LogEvent extends ObservedEvent {
|
|
||||||
const LogEvent({
|
|
||||||
required super.receivedAt,
|
|
||||||
required this.level,
|
|
||||||
required this.message,
|
|
||||||
this.flags = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const traceFlag = 1;
|
|
||||||
static const truncatedFlag = 2;
|
|
||||||
static const coalescedFlag = 4;
|
|
||||||
static const knownFlags = traceFlag | truncatedFlag | coalescedFlag;
|
|
||||||
|
|
||||||
final MavSeverity level;
|
|
||||||
final String message;
|
|
||||||
final int flags;
|
|
||||||
|
|
||||||
bool get isTrace => flags & traceFlag != 0;
|
|
||||||
bool get isTruncated => flags & truncatedFlag != 0;
|
|
||||||
bool get isCoalesced => flags & coalescedFlag != 0;
|
|
||||||
int get unknownFlags => flags & ~knownFlags;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ErrorEvent extends ObservedEvent {
|
|
||||||
const ErrorEvent({
|
|
||||||
required super.receivedAt,
|
|
||||||
required this.path,
|
|
||||||
this.additionalInformation = '',
|
|
||||||
this.flags = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const truncatedFlag = 2;
|
|
||||||
static const coalescedFlag = 4;
|
|
||||||
static const knownFlags = truncatedFlag | coalescedFlag;
|
|
||||||
|
|
||||||
final String path;
|
|
||||||
final String additionalInformation;
|
|
||||||
final int flags;
|
|
||||||
|
|
||||||
bool get isTruncated => flags & truncatedFlag != 0;
|
|
||||||
bool get isCoalesced => flags & coalescedFlag != 0;
|
|
||||||
int get unknownFlags => flags & ~knownFlags;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ShutdownEvent extends ObservedEvent {
|
|
||||||
const ShutdownEvent({required super.receivedAt, required this.reason});
|
|
||||||
|
|
||||||
final KaptanShutdownReason reason;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import 'vehicle_data_update.dart';
|
|
||||||
|
|
||||||
/// A receive-only source of already-classified vehicle observations.
|
|
||||||
///
|
|
||||||
/// The replay implementation is the first concrete port. A live port may be
|
|
||||||
/// added only after the custom MAVLink decoder has its length/CRC/enum safety
|
|
||||||
/// contract and endpoint lifecycle tests.
|
|
||||||
abstract interface class VehicleDataPort {
|
|
||||||
String get label;
|
|
||||||
|
|
||||||
bool get isReplay;
|
|
||||||
|
|
||||||
Stream<VehicleDataUpdate> observe();
|
|
||||||
|
|
||||||
Future<void> close();
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import 'observed_event.dart';
|
|
||||||
|
|
||||||
/// One receive-side outcome from a vehicle-data transport.
|
|
||||||
///
|
|
||||||
/// Expected malformed or unsupported MAVLink frames are reported as drops and
|
|
||||||
/// must not terminate the transport stream. Transport failures remain stream
|
|
||||||
/// errors because the connection lifecycle must handle them separately.
|
|
||||||
sealed class VehicleDataUpdate {
|
|
||||||
const VehicleDataUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A classified observation that is safe to present.
|
|
||||||
final class ObservationReceived extends VehicleDataUpdate {
|
|
||||||
const ObservationReceived(this.event);
|
|
||||||
|
|
||||||
final ObservedEvent event;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ObservationDropKind { parser, semantic }
|
|
||||||
|
|
||||||
/// One or more rejected frames, retained only as bounded accounting.
|
|
||||||
final class ObservationDropped extends VehicleDataUpdate {
|
|
||||||
const ObservationDropped({required this.kind, this.count = 1})
|
|
||||||
: assert(count > 0);
|
|
||||||
|
|
||||||
final ObservationDropKind kind;
|
|
||||||
final int count;
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import '../observations/observed_event.dart';
|
|
||||||
import '../observations/vehicle_data_port.dart';
|
|
||||||
import '../observations/vehicle_data_update.dart';
|
|
||||||
|
|
||||||
/// Deterministic observations for the first Ground Suite workflow.
|
|
||||||
///
|
|
||||||
/// This is intentionally a typed replay rather than a pretend connected
|
|
||||||
/// vehicle. Raw frame replay moves behind the MAVLink decoder once bindings
|
|
||||||
/// are generated from the MAVLink Common and Kaptan dialect definitions.
|
|
||||||
final class ReplayVehicleDataPort implements VehicleDataPort {
|
|
||||||
const ReplayVehicleDataPort();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => 'Replay fixture';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isReplay => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<VehicleDataUpdate> observe() => Stream<VehicleDataUpdate>.fromIterable(
|
|
||||||
_events.map(ObservationReceived.new),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {}
|
|
||||||
|
|
||||||
static final List<ObservedEvent> _events = [
|
|
||||||
HeartbeatEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 12),
|
|
||||||
systemId: 1,
|
|
||||||
componentId: 1,
|
|
||||||
armed: false,
|
|
||||||
vehicleState: 'Standby',
|
|
||||||
),
|
|
||||||
TelemetryEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 12, 0, 1),
|
|
||||||
kind: TelemetryKind.rawImu,
|
|
||||||
),
|
|
||||||
TelemetryEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 12, 0, 2),
|
|
||||||
kind: TelemetryKind.distanceSensor,
|
|
||||||
),
|
|
||||||
LogEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 12, 0, 3),
|
|
||||||
level: MavSeverity.warning,
|
|
||||||
message: 'Range aid stale',
|
|
||||||
flags: LogEvent.coalescedFlag,
|
|
||||||
),
|
|
||||||
ErrorEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 12, 0, 4),
|
|
||||||
path: 'kaptan_drivers::lidar_lite_v3::error::Error::TransferFault',
|
|
||||||
additionalInformation: 'The replay contains a bounded error detail.',
|
|
||||||
flags: ErrorEvent.truncatedFlag,
|
|
||||||
),
|
|
||||||
LogEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 12, 0, 5),
|
|
||||||
level: MavSeverity.info,
|
|
||||||
message: 'Replay fixture complete',
|
|
||||||
),
|
|
||||||
ShutdownEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 12, 0, 6),
|
|
||||||
reason: KaptanShutdownReason.operator,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,616 +0,0 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../fleet/domain/fleet_unit.dart';
|
|
||||||
import '../../vehicle_workspace/domain/vehicle_definition.dart';
|
|
||||||
|
|
||||||
/// A non-modal tactical side panel. Tool state remains in [TacticalMode], so a
|
|
||||||
/// panel can never silently become a map action.
|
|
||||||
enum TacticalPanel { fleetRoster, unitHangar, unitSetup }
|
|
||||||
|
|
||||||
/// Exactly one local map interaction can be active at a time.
|
|
||||||
sealed class TacticalMode {
|
|
||||||
const TacticalMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
class TacticalBrowseMode extends TacticalMode {
|
|
||||||
const TacticalBrowseMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The operator's uncommitted local-unit setup. This is intentionally a panel
|
|
||||||
/// draft rather than a map tool: choosing a category never creates, connects,
|
|
||||||
/// or configures a vehicle.
|
|
||||||
class UnitCreationDraft {
|
|
||||||
const UnitCreationDraft({
|
|
||||||
required this.definition,
|
|
||||||
required this.displayName,
|
|
||||||
});
|
|
||||||
|
|
||||||
final VehicleDefinition definition;
|
|
||||||
final String displayName;
|
|
||||||
|
|
||||||
UnitCreationDraft copyWith({String? displayName}) => UnitCreationDraft(
|
|
||||||
definition: definition,
|
|
||||||
displayName: displayName ?? this.displayName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Session-local freehand drawing tool. It never emits a vehicle command.
|
|
||||||
class TacticalPencilMode extends TacticalMode {
|
|
||||||
TacticalPencilMode({
|
|
||||||
this.hue = 164,
|
|
||||||
this.saturation = .78,
|
|
||||||
this.brightness = .94,
|
|
||||||
this.thickness = 3,
|
|
||||||
List<LocalScenePoint> keyboardPoints = const [],
|
|
||||||
}) : keyboardPoints = List.unmodifiable(keyboardPoints);
|
|
||||||
|
|
||||||
final double hue;
|
|
||||||
final double saturation;
|
|
||||||
final double brightness;
|
|
||||||
final double thickness;
|
|
||||||
final List<LocalScenePoint> keyboardPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A local, freehand mark. It deliberately has no map pin or label: Pencil is
|
|
||||||
/// continuous ink, not a point-placement tool.
|
|
||||||
class LocalSceneDrawing {
|
|
||||||
LocalSceneDrawing({
|
|
||||||
required this.id,
|
|
||||||
required this.hue,
|
|
||||||
required this.saturation,
|
|
||||||
required this.brightness,
|
|
||||||
required this.thickness,
|
|
||||||
required List<LocalScenePoint> points,
|
|
||||||
}) : points = List.unmodifiable(points);
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
final double hue;
|
|
||||||
final double saturation;
|
|
||||||
final double brightness;
|
|
||||||
final double thickness;
|
|
||||||
final List<LocalScenePoint> points;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A local point annotation. It is kept distinct from [LocalSceneDrawing] so
|
|
||||||
/// an accidental Pencil click can never become a named marker.
|
|
||||||
class TacticalMarkerMode extends TacticalMode {
|
|
||||||
const TacticalMarkerMode({
|
|
||||||
this.hue = 26,
|
|
||||||
this.saturation = .68,
|
|
||||||
this.brightness = .98,
|
|
||||||
this.pendingPoint,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double hue;
|
|
||||||
final double saturation;
|
|
||||||
final double brightness;
|
|
||||||
final LocalScenePoint? pendingPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocalSceneMarker {
|
|
||||||
const LocalSceneMarker({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.point,
|
|
||||||
required this.hue,
|
|
||||||
required this.saturation,
|
|
||||||
required this.brightness,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final LocalScenePoint point;
|
|
||||||
final double hue;
|
|
||||||
final double saturation;
|
|
||||||
final double brightness;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TacticalUiState {
|
|
||||||
const TacticalUiState({
|
|
||||||
required this.selectedUnitIds,
|
|
||||||
required this.mode,
|
|
||||||
required this.panel,
|
|
||||||
required this.unitDraft,
|
|
||||||
required this.drawings,
|
|
||||||
required this.markers,
|
|
||||||
required this.selectedDrawingId,
|
|
||||||
required this.selectedMarkerId,
|
|
||||||
required this.overlays,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory TacticalUiState.initial() => const TacticalUiState(
|
|
||||||
selectedUnitIds: {'DEMO-1021'},
|
|
||||||
mode: TacticalBrowseMode(),
|
|
||||||
panel: null,
|
|
||||||
unitDraft: null,
|
|
||||||
drawings: [],
|
|
||||||
markers: [],
|
|
||||||
selectedDrawingId: null,
|
|
||||||
selectedMarkerId: null,
|
|
||||||
overlays: {TacticalMapOverlay.drones},
|
|
||||||
);
|
|
||||||
|
|
||||||
final Set<String> selectedUnitIds;
|
|
||||||
final TacticalMode mode;
|
|
||||||
final TacticalPanel? panel;
|
|
||||||
final UnitCreationDraft? unitDraft;
|
|
||||||
final List<LocalSceneDrawing> drawings;
|
|
||||||
final List<LocalSceneMarker> markers;
|
|
||||||
final String? selectedDrawingId;
|
|
||||||
final String? selectedMarkerId;
|
|
||||||
final Set<TacticalMapOverlay> overlays;
|
|
||||||
|
|
||||||
TacticalUiState copyWith({
|
|
||||||
Set<String>? selectedUnitIds,
|
|
||||||
TacticalMode? mode,
|
|
||||||
TacticalPanel? panel,
|
|
||||||
bool clearPanel = false,
|
|
||||||
UnitCreationDraft? unitDraft,
|
|
||||||
bool clearUnitDraft = false,
|
|
||||||
List<LocalSceneDrawing>? drawings,
|
|
||||||
List<LocalSceneMarker>? markers,
|
|
||||||
String? selectedDrawingId,
|
|
||||||
String? selectedMarkerId,
|
|
||||||
bool clearSelectedDrawing = false,
|
|
||||||
bool clearSelectedMarker = false,
|
|
||||||
Set<TacticalMapOverlay>? overlays,
|
|
||||||
}) => TacticalUiState(
|
|
||||||
selectedUnitIds: selectedUnitIds == null
|
|
||||||
? this.selectedUnitIds
|
|
||||||
: Set.unmodifiable(selectedUnitIds),
|
|
||||||
mode: mode ?? this.mode,
|
|
||||||
panel: clearPanel ? null : panel ?? this.panel,
|
|
||||||
unitDraft: clearUnitDraft ? null : unitDraft ?? this.unitDraft,
|
|
||||||
drawings: drawings == null ? this.drawings : List.unmodifiable(drawings),
|
|
||||||
markers: markers == null ? this.markers : List.unmodifiable(markers),
|
|
||||||
selectedDrawingId: clearSelectedDrawing
|
|
||||||
? null
|
|
||||||
: selectedDrawingId ?? this.selectedDrawingId,
|
|
||||||
selectedMarkerId: clearSelectedMarker
|
|
||||||
? null
|
|
||||||
: selectedMarkerId ?? this.selectedMarkerId,
|
|
||||||
overlays: overlays == null ? this.overlays : Set.unmodifiable(overlays),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only data-backed scene layers belong here. Synthetic sector, route, and
|
|
||||||
/// range-ring artwork is intentionally not part of the tactical workspace.
|
|
||||||
enum TacticalMapOverlay { drones }
|
|
||||||
|
|
||||||
final tacticalControllerProvider =
|
|
||||||
NotifierProvider<TacticalController, TacticalUiState>(
|
|
||||||
TacticalController.new,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Deterministic reducer for session-local tactical interaction state.
|
|
||||||
///
|
|
||||||
/// Agreed action taxonomy:
|
|
||||||
/// - unit selection and placement affect only a local workspace;
|
|
||||||
/// - Pencil affects only [LocalSceneDrawing] values;
|
|
||||||
/// - Marker affects only [LocalSceneMarker] values;
|
|
||||||
/// - overlay actions affect only local rendering.
|
|
||||||
///
|
|
||||||
/// This controller has no transport dependency and no method can emit MAVLink
|
|
||||||
/// or create a live vehicle identity.
|
|
||||||
class TacticalController extends Notifier<TacticalUiState> {
|
|
||||||
int _nextDrawing = 1;
|
|
||||||
int _nextMarker = 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TacticalUiState build() => TacticalUiState.initial();
|
|
||||||
|
|
||||||
void selectUnit(String unitId, {bool toggle = false}) {
|
|
||||||
final selected = toggle ? {...state.selectedUnitIds} : <String>{};
|
|
||||||
if (toggle && selected.contains(unitId)) {
|
|
||||||
selected.remove(unitId);
|
|
||||||
} else {
|
|
||||||
selected.add(unitId);
|
|
||||||
}
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedUnitIds: selected,
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
clearPanel: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces the local visual selection after a drag-box gesture. This is a
|
|
||||||
/// scene-only selection and cannot establish a source or vehicle binding.
|
|
||||||
void selectUnits(Iterable<String> unitIds) => state = state.copyWith(
|
|
||||||
selectedUnitIds: Set.unmodifiable(unitIds.toSet()),
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
clearPanel: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
void clearSelection() => state = state.copyWith(
|
|
||||||
selectedUnitIds: const {},
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
void showFleetRoster() =>
|
|
||||||
state = state.copyWith(panel: TacticalPanel.fleetRoster);
|
|
||||||
|
|
||||||
void closePanel() => state = state.copyWith(clearPanel: true);
|
|
||||||
|
|
||||||
/// Opens the horizontal RTS hangar. The map remains in Browse mode behind
|
|
||||||
/// it; the hangar never becomes a pointer-placement tool.
|
|
||||||
void showUnitHangar() => state = state.copyWith(
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
panel: TacticalPanel.unitHangar,
|
|
||||||
clearUnitDraft: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
void chooseUnitDefinition(
|
|
||||||
VehicleDefinition definition, {
|
|
||||||
required String displayName,
|
|
||||||
}) => state = state.copyWith(
|
|
||||||
panel: TacticalPanel.unitHangar,
|
|
||||||
unitDraft: UnitCreationDraft(
|
|
||||||
definition: definition,
|
|
||||||
displayName: displayName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
void updateUnitDraftName(String displayName) {
|
|
||||||
final draft = state.unitDraft;
|
|
||||||
if (draft == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(unitDraft: draft.copyWith(displayName: displayName));
|
|
||||||
}
|
|
||||||
|
|
||||||
void beginUnitSetup() {
|
|
||||||
if (state.unitDraft == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(panel: TacticalPanel.unitSetup);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns to the catalog without discarding the selected category or its
|
|
||||||
/// local name draft. Closing the hangar is the explicit discard boundary.
|
|
||||||
void returnToUnitHangar() =>
|
|
||||||
state = state.copyWith(panel: TacticalPanel.unitHangar);
|
|
||||||
|
|
||||||
/// Opens setup for the one already-selected local workspace unit. The ID is
|
|
||||||
/// supplied by the presentation layer after it verifies the unit exists in
|
|
||||||
/// its workspace; this reducer only accepts a matching tactical selection.
|
|
||||||
void showUnitSetupForSelectedLocalUnit(String localUnitId) {
|
|
||||||
if (state.unitDraft != null ||
|
|
||||||
state.selectedUnitIds.length != 1 ||
|
|
||||||
!state.selectedUnitIds.contains(localUnitId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
panel: TacticalPanel.unitSetup,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void closeUnitHangar() =>
|
|
||||||
state = state.copyWith(clearPanel: true, clearUnitDraft: true);
|
|
||||||
|
|
||||||
void completeUnitPlacement(String unitId) => state = state.copyWith(
|
|
||||||
selectedUnitIds: {unitId},
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
clearPanel: true,
|
|
||||||
clearUnitDraft: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
void beginPencil() => state = state.copyWith(
|
|
||||||
mode: TacticalPencilMode(),
|
|
||||||
selectedUnitIds: const {},
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
clearPanel: true,
|
|
||||||
clearUnitDraft: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
void configurePencil({
|
|
||||||
double? hue,
|
|
||||||
double? saturation,
|
|
||||||
double? brightness,
|
|
||||||
double? thickness,
|
|
||||||
}) {
|
|
||||||
final mode = state.mode;
|
|
||||||
if (mode is! TacticalPencilMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(
|
|
||||||
mode: TacticalPencilMode(
|
|
||||||
hue: hue ?? mode.hue,
|
|
||||||
saturation: saturation ?? mode.saturation,
|
|
||||||
brightness: brightness ?? mode.brightness,
|
|
||||||
thickness: thickness ?? mode.thickness,
|
|
||||||
keyboardPoints: mode.keyboardPoints,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accessible alternative to a direct pointer trace. Keyboard users add
|
|
||||||
/// points at a movable scene cursor; it never exposes a grid or location.
|
|
||||||
void addKeyboardPencilPoint(LocalScenePoint point) {
|
|
||||||
final mode = state.mode;
|
|
||||||
if (mode is! TacticalPencilMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(
|
|
||||||
mode: TacticalPencilMode(
|
|
||||||
hue: mode.hue,
|
|
||||||
saturation: mode.saturation,
|
|
||||||
brightness: mode.brightness,
|
|
||||||
thickness: mode.thickness,
|
|
||||||
keyboardPoints: [...mode.keyboardPoints, point],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get canFinishKeyboardPencil {
|
|
||||||
final mode = state.mode;
|
|
||||||
return mode is TacticalPencilMode && mode.keyboardPoints.length >= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool finishKeyboardPencil() {
|
|
||||||
final mode = state.mode;
|
|
||||||
if (mode is! TacticalPencilMode || !canFinishKeyboardPencil) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_commitDrawing(
|
|
||||||
hue: mode.hue,
|
|
||||||
saturation: mode.saturation,
|
|
||||||
brightness: mode.brightness,
|
|
||||||
thickness: mode.thickness,
|
|
||||||
points: mode.keyboardPoints,
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commits a direct press-drag-release map gesture. Preview samples stay in
|
|
||||||
/// the input widget; only the simplified stroke becomes durable local scene
|
|
||||||
/// geometry here.
|
|
||||||
bool commitPencilStroke(List<LocalScenePoint> points) {
|
|
||||||
final mode = state.mode;
|
|
||||||
if (mode is! TacticalPencilMode) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (points.length < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_commitDrawing(
|
|
||||||
hue: mode.hue,
|
|
||||||
saturation: mode.saturation,
|
|
||||||
brightness: mode.brightness,
|
|
||||||
thickness: mode.thickness,
|
|
||||||
points: _decimateStroke(points),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void beginMarker() => state = state.copyWith(
|
|
||||||
mode: const TacticalMarkerMode(),
|
|
||||||
selectedUnitIds: const {},
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
clearPanel: true,
|
|
||||||
clearUnitDraft: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
void configureMarker({double? hue, double? saturation, double? brightness}) {
|
|
||||||
final mode = state.mode;
|
|
||||||
if (mode is! TacticalMarkerMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(
|
|
||||||
mode: TacticalMarkerMode(
|
|
||||||
hue: hue ?? mode.hue,
|
|
||||||
saturation: saturation ?? mode.saturation,
|
|
||||||
brightness: brightness ?? mode.brightness,
|
|
||||||
pendingPoint: mode.pendingPoint,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void chooseMarkerPoint(LocalScenePoint point) {
|
|
||||||
final mode = state.mode;
|
|
||||||
if (mode is! TacticalMarkerMode || mode.pendingPoint != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(
|
|
||||||
mode: TacticalMarkerMode(
|
|
||||||
hue: mode.hue,
|
|
||||||
saturation: mode.saturation,
|
|
||||||
brightness: mode.brightness,
|
|
||||||
pendingPoint: point,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns false when the label is not acceptable. Labels are local plain
|
|
||||||
/// text only, deliberately bounded before they can reach a map renderer.
|
|
||||||
bool commitMarkerName(String value) {
|
|
||||||
final mode = state.mode;
|
|
||||||
final name = value.trim();
|
|
||||||
if (mode is! TacticalMarkerMode ||
|
|
||||||
mode.pendingPoint == null ||
|
|
||||||
name.isEmpty ||
|
|
||||||
name.runes.length > 48) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final marker = LocalSceneMarker(
|
|
||||||
id: 'local-marker-${_nextMarker++}',
|
|
||||||
name: name,
|
|
||||||
point: mode.pendingPoint!,
|
|
||||||
hue: mode.hue,
|
|
||||||
saturation: mode.saturation,
|
|
||||||
brightness: mode.brightness,
|
|
||||||
);
|
|
||||||
state = state.copyWith(
|
|
||||||
markers: [...state.markers, marker],
|
|
||||||
selectedMarkerId: marker.id,
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
mode: TacticalMarkerMode(
|
|
||||||
hue: mode.hue,
|
|
||||||
saturation: mode.saturation,
|
|
||||||
brightness: mode.brightness,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// First cancellation closes an unfinished marker name prompt; the next
|
|
||||||
/// returns to Select. This gives Escape/right click a predictable ladder.
|
|
||||||
bool cancelActiveTool() {
|
|
||||||
final mode = state.mode;
|
|
||||||
if (mode is TacticalMarkerMode && mode.pendingPoint != null) {
|
|
||||||
state = state.copyWith(
|
|
||||||
mode: TacticalMarkerMode(
|
|
||||||
hue: mode.hue,
|
|
||||||
saturation: mode.saturation,
|
|
||||||
brightness: mode.brightness,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
cancelMode();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectDrawing(String drawingId) => state = state.copyWith(
|
|
||||||
selectedUnitIds: const {},
|
|
||||||
selectedDrawingId: drawingId,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
);
|
|
||||||
|
|
||||||
void selectMarker(String markerId) => state = state.copyWith(
|
|
||||||
selectedUnitIds: const {},
|
|
||||||
selectedMarkerId: markerId,
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
);
|
|
||||||
|
|
||||||
void removeSelectedSceneObject() {
|
|
||||||
final drawing = state.selectedDrawingId;
|
|
||||||
final marker = state.selectedMarkerId;
|
|
||||||
if (drawing != null) {
|
|
||||||
state = state.copyWith(
|
|
||||||
drawings: state.drawings.where((value) => value.id != drawing).toList(),
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (marker == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state.copyWith(
|
|
||||||
markers: state.markers.where((value) => value.id != marker).toList(),
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearSceneObjects() => state = state.copyWith(
|
|
||||||
drawings: const [],
|
|
||||||
markers: const [],
|
|
||||||
clearSelectedDrawing: true,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
void cancelMode() => state = state.copyWith(mode: const TacticalBrowseMode());
|
|
||||||
|
|
||||||
void toggleOverlay(TacticalMapOverlay overlay, bool visible) {
|
|
||||||
final overlays = {...state.overlays};
|
|
||||||
if (visible) {
|
|
||||||
overlays.add(overlay);
|
|
||||||
} else {
|
|
||||||
overlays.remove(overlay);
|
|
||||||
}
|
|
||||||
state = state.copyWith(overlays: overlays);
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetLocalView() => state = state.copyWith(
|
|
||||||
overlays: {TacticalMapOverlay.drones},
|
|
||||||
mode: const TacticalBrowseMode(),
|
|
||||||
);
|
|
||||||
|
|
||||||
void _commitDrawing({
|
|
||||||
required double hue,
|
|
||||||
required double saturation,
|
|
||||||
required double brightness,
|
|
||||||
required double thickness,
|
|
||||||
required List<LocalScenePoint> points,
|
|
||||||
}) {
|
|
||||||
final drawing = LocalSceneDrawing(
|
|
||||||
id: 'local-drawing-${_nextDrawing++}',
|
|
||||||
hue: hue,
|
|
||||||
saturation: saturation,
|
|
||||||
brightness: brightness,
|
|
||||||
thickness: thickness,
|
|
||||||
points: List.unmodifiable(points),
|
|
||||||
);
|
|
||||||
state = state.copyWith(
|
|
||||||
drawings: [...state.drawings, drawing],
|
|
||||||
selectedDrawingId: drawing.id,
|
|
||||||
clearSelectedMarker: true,
|
|
||||||
mode: TacticalPencilMode(
|
|
||||||
hue: hue,
|
|
||||||
saturation: saturation,
|
|
||||||
brightness: brightness,
|
|
||||||
thickness: thickness,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve enough detail for a visibly hand-drawn line while keeping local
|
|
||||||
// session state bounded. Input preview has its own higher short-lived cap.
|
|
||||||
static const _maximumStrokePoints = 256;
|
|
||||||
|
|
||||||
List<LocalScenePoint> _decimateStroke(List<LocalScenePoint> points) {
|
|
||||||
if (points.length <= _maximumStrokePoints) {
|
|
||||||
return List.unmodifiable(points);
|
|
||||||
}
|
|
||||||
return List.unmodifiable([
|
|
||||||
for (var index = 0; index < _maximumStrokePoints; index++)
|
|
||||||
points[((points.length - 1) * index / (_maximumStrokePoints - 1))
|
|
||||||
.round()],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pointer preview is isolated from command/selection state. A map widget may
|
|
||||||
/// update it once per frame while a placement tool is active.
|
|
||||||
class TacticalPointerPreview {
|
|
||||||
TacticalPointerPreview({this.point, List<LocalScenePoint> stroke = const []})
|
|
||||||
: stroke = List.unmodifiable(stroke);
|
|
||||||
|
|
||||||
final LocalScenePoint? point;
|
|
||||||
final List<LocalScenePoint> stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
final tacticalPointerPreviewProvider =
|
|
||||||
NotifierProvider<TacticalPointerPreviewController, TacticalPointerPreview>(
|
|
||||||
TacticalPointerPreviewController.new,
|
|
||||||
);
|
|
||||||
|
|
||||||
class TacticalPointerPreviewController
|
|
||||||
extends Notifier<TacticalPointerPreview> {
|
|
||||||
@override
|
|
||||||
TacticalPointerPreview build() => TacticalPointerPreview();
|
|
||||||
|
|
||||||
void show(LocalScenePoint point, {List<LocalScenePoint> stroke = const []}) =>
|
|
||||||
state = TacticalPointerPreview(point: point, stroke: stroke);
|
|
||||||
|
|
||||||
void nudge(double dx, double dy) {
|
|
||||||
final current = state.point ?? const LocalScenePoint(x: .5, y: .5);
|
|
||||||
show(
|
|
||||||
LocalScenePoint(
|
|
||||||
x: (current.x + dx).clamp(0.0, 1.0),
|
|
||||||
y: (current.y + dy).clamp(0.0, 1.0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() => state = TacticalPointerPreview();
|
|
||||||
}
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../../core/observations/observed_event.dart';
|
|
||||||
import '../../../shared/widgets/severity_badge.dart';
|
|
||||||
import '../../../shared/widgets/surfaces.dart';
|
|
||||||
import '../../observability/presentation/observability_controller.dart';
|
|
||||||
|
|
||||||
enum _DiagnosticFilter { all, logs, errors, shutdowns }
|
|
||||||
|
|
||||||
class DiagnosticsPage extends ConsumerStatefulWidget {
|
|
||||||
const DiagnosticsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<DiagnosticsPage> createState() => _DiagnosticsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DiagnosticsPageState extends ConsumerState<DiagnosticsPage> {
|
|
||||||
_DiagnosticFilter _filter = _DiagnosticFilter.all;
|
|
||||||
String _query = '';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final state = ref.watch(observabilityControllerProvider);
|
|
||||||
final diagnostics = state.events
|
|
||||||
.where(_isDiagnostic)
|
|
||||||
.toList(growable: false);
|
|
||||||
final events = diagnostics.where(_matches).toList(growable: false);
|
|
||||||
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(28, 28, 28, 16),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const ConsoleLabel('Event stream'),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Event timeline',
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
'Read-only decoded observations. Diagnostic delivery can be bounded and lossy.',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Retained diagnostics ${state.events.length} · local records not retained ${state.localTimelineEvictionCount}',
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodySmall?.copyWith(fontFamily: 'monospace'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: [
|
|
||||||
for (final filter in _DiagnosticFilter.values)
|
|
||||||
ChoiceChip(
|
|
||||||
label: Text(_filterLabel(filter, diagnostics)),
|
|
||||||
selected: _filter == filter,
|
|
||||||
onSelected: (_) => setState(() => _filter = filter),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Search message, error path, or detail',
|
|
||||||
prefixIcon: Icon(Icons.search),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() => _query = value.trim().toLowerCase());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (events.isEmpty)
|
|
||||||
const SliverPadding(
|
|
||||||
padding: EdgeInsets.fromLTRB(28, 0, 28, 28),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: InstrumentPanel(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(20),
|
|
||||||
child: Text('No events match this view.'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(28, 0, 28, 28),
|
|
||||||
sliver: SliverList.builder(
|
|
||||||
itemCount: events.length,
|
|
||||||
itemBuilder: (context, index) =>
|
|
||||||
_DiagnosticRow(event: events[index]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _matches(ObservedEvent event) {
|
|
||||||
final typeMatches = switch (_filter) {
|
|
||||||
_DiagnosticFilter.all => true,
|
|
||||||
_DiagnosticFilter.logs => event is LogEvent,
|
|
||||||
_DiagnosticFilter.errors => event is ErrorEvent,
|
|
||||||
_DiagnosticFilter.shutdowns => event is ShutdownEvent,
|
|
||||||
};
|
|
||||||
return typeMatches &&
|
|
||||||
(_query.isEmpty ||
|
|
||||||
_searchableText(event).toLowerCase().contains(_query));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DiagnosticRow extends StatelessWidget {
|
|
||||||
const _DiagnosticRow({required this.event});
|
|
||||||
|
|
||||||
final ObservedEvent event;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final content = _contentFor(
|
|
||||||
event,
|
|
||||||
Theme.of(context).brightness == Brightness.dark,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Semantics(
|
|
||||||
container: true,
|
|
||||||
label: content.semanticLabel,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: InstrumentPanel(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(width: 3, height: 56, color: content.tone),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
SizedBox(
|
|
||||||
width: 92,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ConsoleLabel(content.kind, color: content.tone),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
_timelineTimestamp(event.receivedAt),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
content.icon,
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: SelectionArea(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
content.title,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
content.detail,
|
|
||||||
style: content.monoDetail
|
|
||||||
? Theme.of(context).textTheme.bodyMedium
|
|
||||||
?.copyWith(fontFamily: 'monospace')
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
if (content.additionalDetail case final detail?) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
detail,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DiagnosticContent {
|
|
||||||
const _DiagnosticContent({
|
|
||||||
required this.kind,
|
|
||||||
required this.tone,
|
|
||||||
required this.icon,
|
|
||||||
required this.title,
|
|
||||||
required this.detail,
|
|
||||||
required this.semanticLabel,
|
|
||||||
this.additionalDetail,
|
|
||||||
this.monoDetail = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String kind;
|
|
||||||
final Color tone;
|
|
||||||
final Widget icon;
|
|
||||||
final String title;
|
|
||||||
final String detail;
|
|
||||||
final String? additionalDetail;
|
|
||||||
final String semanticLabel;
|
|
||||||
final bool monoDetail;
|
|
||||||
}
|
|
||||||
|
|
||||||
_DiagnosticContent _contentFor(
|
|
||||||
ObservedEvent event,
|
|
||||||
bool dark,
|
|
||||||
) => switch (event) {
|
|
||||||
LogEvent(:final level, :final message, :final flags) => _DiagnosticContent(
|
|
||||||
kind: 'Log',
|
|
||||||
tone: _toneForSeverity(level, dark),
|
|
||||||
icon: SeverityBadge(level: level),
|
|
||||||
title: level.label,
|
|
||||||
detail: message,
|
|
||||||
additionalDetail: _formatLogFlags(flags),
|
|
||||||
semanticLabel: _semanticLogLabel(level, message, flags),
|
|
||||||
),
|
|
||||||
ErrorEvent(:final path, :final additionalInformation, :final flags) =>
|
|
||||||
_DiagnosticContent(
|
|
||||||
kind: 'Error',
|
|
||||||
tone: dark ? const Color(0xffff6b63) : const Color(0xffd92d20),
|
|
||||||
icon: Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
color: dark ? const Color(0xffff6b63) : const Color(0xffd92d20),
|
|
||||||
),
|
|
||||||
title: 'Runtime error observed',
|
|
||||||
detail: path,
|
|
||||||
additionalDetail: _formatErrorDetail(additionalInformation, flags),
|
|
||||||
semanticLabel: _semanticErrorLabel(path, additionalInformation, flags),
|
|
||||||
monoDetail: true,
|
|
||||||
),
|
|
||||||
ShutdownEvent(:final reason) => _DiagnosticContent(
|
|
||||||
kind: 'Shutdown',
|
|
||||||
tone: dark ? const Color(0xfff7b955) : const Color(0xffb54708),
|
|
||||||
icon: Icon(
|
|
||||||
Icons.power_settings_new_outlined,
|
|
||||||
color: dark ? const Color(0xfff7b955) : const Color(0xffb54708),
|
|
||||||
),
|
|
||||||
title: 'Orderly shutdown report',
|
|
||||||
detail: 'Reason: ${reason.label}',
|
|
||||||
additionalDetail:
|
|
||||||
'Completed orderly, output-inhibited report; not a command, panic, hard-fault, or reset.',
|
|
||||||
semanticLabel: 'Orderly shutdown report: ${reason.label}',
|
|
||||||
),
|
|
||||||
_ => throw StateError('only diagnostic events reach the timeline'),
|
|
||||||
};
|
|
||||||
|
|
||||||
bool _isDiagnostic(ObservedEvent event) =>
|
|
||||||
event is LogEvent || event is ErrorEvent || event is ShutdownEvent;
|
|
||||||
|
|
||||||
String _filterLabel(_DiagnosticFilter filter, List<ObservedEvent> events) {
|
|
||||||
final count = switch (filter) {
|
|
||||||
_DiagnosticFilter.all => events.length,
|
|
||||||
_DiagnosticFilter.logs => events.whereType<LogEvent>().length,
|
|
||||||
_DiagnosticFilter.errors => events.whereType<ErrorEvent>().length,
|
|
||||||
_DiagnosticFilter.shutdowns => events.whereType<ShutdownEvent>().length,
|
|
||||||
};
|
|
||||||
final label = switch (filter) {
|
|
||||||
_DiagnosticFilter.all => 'All events',
|
|
||||||
_DiagnosticFilter.logs => 'Logs',
|
|
||||||
_DiagnosticFilter.errors => 'Errors',
|
|
||||||
_DiagnosticFilter.shutdowns => 'Shutdown reports',
|
|
||||||
};
|
|
||||||
return '$label $count';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _searchableText(ObservedEvent event) => switch (event) {
|
|
||||||
LogEvent(:final level, :final message) => '${level.label} $message',
|
|
||||||
ErrorEvent(:final path, :final additionalInformation) =>
|
|
||||||
'$path $additionalInformation',
|
|
||||||
ShutdownEvent(:final reason) => reason.label,
|
|
||||||
_ => '',
|
|
||||||
};
|
|
||||||
|
|
||||||
Color _toneForSeverity(MavSeverity level, bool dark) =>
|
|
||||||
switch (level.wireValue) {
|
|
||||||
0 ||
|
|
||||||
1 ||
|
|
||||||
2 ||
|
|
||||||
3 => dark ? const Color(0xffff6b63) : const Color(0xffd92d20),
|
|
||||||
4 => dark ? const Color(0xfff7b955) : const Color(0xffb54708),
|
|
||||||
5 || 6 => dark ? const Color(0xff70b8ff) : const Color(0xff175cd3),
|
|
||||||
7 => dark ? const Color(0xffa8b5ac) : const Color(0xff546258),
|
|
||||||
_ => dark ? const Color(0xffa8b5ac) : const Color(0xff546258),
|
|
||||||
};
|
|
||||||
|
|
||||||
String? _formatLogFlags(int flags) {
|
|
||||||
final labels = <String>[
|
|
||||||
if (flags & LogEvent.traceFlag != 0) 'Trace',
|
|
||||||
if (flags & LogEvent.truncatedFlag != 0) 'Truncated',
|
|
||||||
if (flags & LogEvent.coalescedFlag != 0) 'Coalesced',
|
|
||||||
if (flags & ~LogEvent.knownFlags != 0)
|
|
||||||
'Unknown bits 0x${(flags & ~LogEvent.knownFlags).toRadixString(16)}',
|
|
||||||
];
|
|
||||||
return labels.isEmpty ? null : 'Flags: ${labels.join(' · ')}';
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _formatErrorDetail(String additionalInformation, int flags) {
|
|
||||||
final flagDetail = _formatErrorFlags(flags);
|
|
||||||
final details = <String>[
|
|
||||||
if (additionalInformation.isNotEmpty) additionalInformation,
|
|
||||||
?flagDetail,
|
|
||||||
];
|
|
||||||
return details.isEmpty ? null : details.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _formatErrorFlags(int flags) {
|
|
||||||
final labels = <String>[
|
|
||||||
if (flags & ErrorEvent.truncatedFlag != 0) 'Truncated',
|
|
||||||
if (flags & ErrorEvent.coalescedFlag != 0) 'Coalesced',
|
|
||||||
if (flags & ~ErrorEvent.knownFlags != 0)
|
|
||||||
'Unknown bits 0x${(flags & ~ErrorEvent.knownFlags).toRadixString(16)}',
|
|
||||||
];
|
|
||||||
return labels.isEmpty ? null : 'Flags: ${labels.join(' · ')}';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _semanticLogLabel(MavSeverity level, String message, int flags) {
|
|
||||||
final flagDetail = _formatLogFlags(flags);
|
|
||||||
return flagDetail == null
|
|
||||||
? '${level.label} log: $message'
|
|
||||||
: '${level.label} log: $message. $flagDetail';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _semanticErrorLabel(
|
|
||||||
String path,
|
|
||||||
String additionalInformation,
|
|
||||||
int flags,
|
|
||||||
) {
|
|
||||||
final detail = _formatErrorDetail(additionalInformation, flags);
|
|
||||||
return detail == null ? 'Error: $path' : 'Error: $path. $detail';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _timelineTimestamp(DateTime value) =>
|
|
||||||
'${value.hour.toString().padLeft(2, '0')}:'
|
|
||||||
'${value.minute.toString().padLeft(2, '0')}:'
|
|
||||||
'${value.second.toString().padLeft(2, '0')}Z';
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import '../../vehicle_workspace/domain/vehicle_definition.dart';
|
|
||||||
|
|
||||||
/// An operator-created fleet unit in the local planning workspace.
|
|
||||||
///
|
|
||||||
/// It deliberately contains no received identity, platform, authority, or
|
|
||||||
/// physical pose. Those must arrive through separate reviewed bindings later.
|
|
||||||
class FleetUnit {
|
|
||||||
const FleetUnit({
|
|
||||||
required this.localUnitId,
|
|
||||||
required this.displayName,
|
|
||||||
required this.definition,
|
|
||||||
required this.localPlacement,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String localUnitId;
|
|
||||||
final String displayName;
|
|
||||||
final VehicleDefinition definition;
|
|
||||||
final LocalScenePoint localPlacement;
|
|
||||||
|
|
||||||
FleetUnit copyWith({String? displayName}) => FleetUnit(
|
|
||||||
localUnitId: localUnitId,
|
|
||||||
displayName: displayName ?? this.displayName,
|
|
||||||
definition: definition,
|
|
||||||
localPlacement: localPlacement,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A normalized placement in the operator's local tactical scene.
|
|
||||||
///
|
|
||||||
/// This is deliberately not a geographic position, vehicle pose, or received
|
|
||||||
/// location. A future map binding will keep reported coordinates separate.
|
|
||||||
class LocalScenePoint {
|
|
||||||
const LocalScenePoint({required this.x, required this.y})
|
|
||||||
: assert(x >= 0 && x <= 1),
|
|
||||||
assert(y >= 0 && y <= 1);
|
|
||||||
|
|
||||||
final double x;
|
|
||||||
final double y;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is LocalScenePoint && other.x == x && other.y == y;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(x, y);
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../../core/observations/observed_event.dart';
|
|
||||||
import '../../../core/observations/vehicle_data_port.dart';
|
|
||||||
import '../../../core/observations/vehicle_data_update.dart';
|
|
||||||
import '../../../core/transport/replay_vehicle_data_port.dart';
|
|
||||||
|
|
||||||
final vehicleDataPortProvider = Provider<VehicleDataPort>((ref) {
|
|
||||||
final port = const ReplayVehicleDataPort();
|
|
||||||
ref.onDispose(() => unawaited(port.close()));
|
|
||||||
return port;
|
|
||||||
});
|
|
||||||
|
|
||||||
final observabilityControllerProvider =
|
|
||||||
NotifierProvider<ObservabilityController, ObservabilityState>(
|
|
||||||
ObservabilityController.new,
|
|
||||||
);
|
|
||||||
|
|
||||||
class ObservabilityController extends Notifier<ObservabilityState> {
|
|
||||||
static const _criticalAnnouncementWindow = Duration(seconds: 2);
|
|
||||||
static const _timelineCapacity = 500;
|
|
||||||
|
|
||||||
StreamSubscription<VehicleDataUpdate>? _subscription;
|
|
||||||
DateTime? _lastCriticalAnnouncementAt;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ObservabilityState build() {
|
|
||||||
final source = ref.read(vehicleDataPortProvider);
|
|
||||||
ref.onDispose(_cancelSubscription);
|
|
||||||
Future<void>.microtask(() => _observe(source));
|
|
||||||
return ObservabilityState.initial(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _observe(VehicleDataPort source) {
|
|
||||||
_subscription = source.observe().listen(
|
|
||||||
_handleUpdate,
|
|
||||||
onError: (Object _, StackTrace _) {
|
|
||||||
state = state.copyWith(
|
|
||||||
status: ObservationStatus.disconnected,
|
|
||||||
accessibilityAnnouncement: 'Observation connection closed.',
|
|
||||||
replaceAccessibilityAnnouncement: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
state = state.copyWith(
|
|
||||||
status: source.isReplay
|
|
||||||
? ObservationStatus.replayComplete
|
|
||||||
: ObservationStatus.disconnected,
|
|
||||||
accessibilityAnnouncement: source.isReplay
|
|
||||||
? 'Replay complete.'
|
|
||||||
: 'Observation connection closed.',
|
|
||||||
replaceAccessibilityAnnouncement: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleUpdate(VehicleDataUpdate update) {
|
|
||||||
switch (update) {
|
|
||||||
case ObservationReceived(:final event):
|
|
||||||
_record(event);
|
|
||||||
case ObservationDropped(:final kind, :final count):
|
|
||||||
state = state.copyWith(
|
|
||||||
status: ObservationStatus.receiving,
|
|
||||||
parserDropCount:
|
|
||||||
state.parserDropCount +
|
|
||||||
(kind == ObservationDropKind.parser ? count : 0),
|
|
||||||
semanticDropCount:
|
|
||||||
state.semanticDropCount +
|
|
||||||
(kind == ObservationDropKind.semantic ? count : 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _record(ObservedEvent event) {
|
|
||||||
final announcement = _announcementFor(event);
|
|
||||||
final retention = _isTimelineEvent(event)
|
|
||||||
? _retainTimelineEvent(state.events, event)
|
|
||||||
: _TimelineRetention(state.events);
|
|
||||||
final heartbeat = event is HeartbeatEvent ? event : state.lastHeartbeat;
|
|
||||||
final observedStreams = switch (event) {
|
|
||||||
TelemetryEvent(:final kind) => {...state.observedTelemetryKinds, kind},
|
|
||||||
_ => state.observedTelemetryKinds,
|
|
||||||
};
|
|
||||||
state = state.copyWith(
|
|
||||||
status: ObservationStatus.receiving,
|
|
||||||
lastFrameAt: event.receivedAt,
|
|
||||||
events: retention.events,
|
|
||||||
lastHeartbeat: heartbeat,
|
|
||||||
observedTelemetryKinds: Set.unmodifiable(observedStreams),
|
|
||||||
localTimelineEvictionCount:
|
|
||||||
state.localTimelineEvictionCount + retention.evictionCount,
|
|
||||||
accessibilityAnnouncement: announcement.message,
|
|
||||||
replaceAccessibilityAnnouncement: true,
|
|
||||||
coalescedAlertCount:
|
|
||||||
state.coalescedAlertCount + (announcement.coalesced ? 1 : 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_AnnouncementDecision _announcementFor(ObservedEvent event) {
|
|
||||||
final candidate = switch (event) {
|
|
||||||
ErrorEvent(:final path) => 'Error received: $path',
|
|
||||||
ShutdownEvent(:final reason) =>
|
|
||||||
'Orderly shutdown observed: ${reason.label}',
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
if (candidate == null) {
|
|
||||||
return const _AnnouncementDecision();
|
|
||||||
}
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
final previous = _lastCriticalAnnouncementAt;
|
|
||||||
if (previous != null &&
|
|
||||||
now.difference(previous) < _criticalAnnouncementWindow) {
|
|
||||||
return const _AnnouncementDecision(coalesced: true);
|
|
||||||
}
|
|
||||||
_lastCriticalAnnouncementAt = now;
|
|
||||||
return _AnnouncementDecision(message: candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _cancelSubscription() {
|
|
||||||
_subscription?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
_TimelineRetention _retainTimelineEvent(
|
|
||||||
List<ObservedEvent> current,
|
|
||||||
ObservedEvent incoming,
|
|
||||||
) {
|
|
||||||
if (current.length < _timelineCapacity) {
|
|
||||||
return _TimelineRetention([...current, incoming]);
|
|
||||||
}
|
|
||||||
|
|
||||||
final retained = List<ObservedEvent>.of(current);
|
|
||||||
final optionalIndex = retained.indexWhere((event) => !_isCritical(event));
|
|
||||||
if (optionalIndex >= 0) {
|
|
||||||
retained.removeAt(optionalIndex);
|
|
||||||
retained.add(incoming);
|
|
||||||
return _TimelineRetention(retained, evictionCount: 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isCritical(incoming)) {
|
|
||||||
retained.removeAt(0);
|
|
||||||
retained.add(incoming);
|
|
||||||
return _TimelineRetention(retained, evictionCount: 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _TimelineRetention(retained, evictionCount: 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isCritical(ObservedEvent event) =>
|
|
||||||
event is ErrorEvent || event is ShutdownEvent;
|
|
||||||
|
|
||||||
bool _isTimelineEvent(ObservedEvent event) =>
|
|
||||||
event is LogEvent || _isCritical(event);
|
|
||||||
|
|
||||||
class _TimelineRetention {
|
|
||||||
_TimelineRetention(Iterable<ObservedEvent> events, {this.evictionCount = 0})
|
|
||||||
: events = List.unmodifiable(events);
|
|
||||||
|
|
||||||
final List<ObservedEvent> events;
|
|
||||||
final int evictionCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ObservationStatus {
|
|
||||||
starting('Starting'),
|
|
||||||
receiving('Observing'),
|
|
||||||
replayComplete('Replay complete'),
|
|
||||||
disconnected('Disconnected');
|
|
||||||
|
|
||||||
const ObservationStatus(this.label);
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObservabilityState {
|
|
||||||
const ObservabilityState({
|
|
||||||
required this.sourceLabel,
|
|
||||||
required this.isReplay,
|
|
||||||
required this.status,
|
|
||||||
required this.events,
|
|
||||||
required this.lastHeartbeat,
|
|
||||||
required this.observedTelemetryKinds,
|
|
||||||
required this.parserDropCount,
|
|
||||||
required this.semanticDropCount,
|
|
||||||
required this.coalescedAlertCount,
|
|
||||||
required this.localTimelineEvictionCount,
|
|
||||||
this.lastFrameAt,
|
|
||||||
this.accessibilityAnnouncement,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory ObservabilityState.initial(VehicleDataPort source) {
|
|
||||||
return ObservabilityState(
|
|
||||||
sourceLabel: source.label,
|
|
||||||
isReplay: source.isReplay,
|
|
||||||
status: ObservationStatus.starting,
|
|
||||||
events: const [],
|
|
||||||
lastHeartbeat: null,
|
|
||||||
observedTelemetryKinds: const {},
|
|
||||||
parserDropCount: 0,
|
|
||||||
semanticDropCount: 0,
|
|
||||||
coalescedAlertCount: 0,
|
|
||||||
localTimelineEvictionCount: 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String sourceLabel;
|
|
||||||
final bool isReplay;
|
|
||||||
final ObservationStatus status;
|
|
||||||
final List<ObservedEvent> events;
|
|
||||||
final HeartbeatEvent? lastHeartbeat;
|
|
||||||
final Set<TelemetryKind> observedTelemetryKinds;
|
|
||||||
final int parserDropCount;
|
|
||||||
final int semanticDropCount;
|
|
||||||
final int coalescedAlertCount;
|
|
||||||
final int localTimelineEvictionCount;
|
|
||||||
final DateTime? lastFrameAt;
|
|
||||||
final String? accessibilityAnnouncement;
|
|
||||||
|
|
||||||
ObservabilityState copyWith({
|
|
||||||
ObservationStatus? status,
|
|
||||||
List<ObservedEvent>? events,
|
|
||||||
HeartbeatEvent? lastHeartbeat,
|
|
||||||
Set<TelemetryKind>? observedTelemetryKinds,
|
|
||||||
int? parserDropCount,
|
|
||||||
int? semanticDropCount,
|
|
||||||
int? coalescedAlertCount,
|
|
||||||
int? localTimelineEvictionCount,
|
|
||||||
DateTime? lastFrameAt,
|
|
||||||
String? accessibilityAnnouncement,
|
|
||||||
bool replaceAccessibilityAnnouncement = false,
|
|
||||||
}) {
|
|
||||||
return ObservabilityState(
|
|
||||||
sourceLabel: sourceLabel,
|
|
||||||
isReplay: isReplay,
|
|
||||||
status: status ?? this.status,
|
|
||||||
events: events ?? this.events,
|
|
||||||
lastHeartbeat: lastHeartbeat ?? this.lastHeartbeat,
|
|
||||||
observedTelemetryKinds:
|
|
||||||
observedTelemetryKinds ?? this.observedTelemetryKinds,
|
|
||||||
parserDropCount: parserDropCount ?? this.parserDropCount,
|
|
||||||
semanticDropCount: semanticDropCount ?? this.semanticDropCount,
|
|
||||||
coalescedAlertCount: coalescedAlertCount ?? this.coalescedAlertCount,
|
|
||||||
localTimelineEvictionCount:
|
|
||||||
localTimelineEvictionCount ?? this.localTimelineEvictionCount,
|
|
||||||
lastFrameAt: lastFrameAt ?? this.lastFrameAt,
|
|
||||||
accessibilityAnnouncement: replaceAccessibilityAnnouncement
|
|
||||||
? accessibilityAnnouncement
|
|
||||||
: this.accessibilityAnnouncement,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnnouncementDecision {
|
|
||||||
const _AnnouncementDecision({this.message, this.coalesced = false});
|
|
||||||
|
|
||||||
final String? message;
|
|
||||||
final bool coalesced;
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import 'vehicle_definition.dart';
|
|
||||||
|
|
||||||
/// Revision of the local reviewed definition catalog.
|
|
||||||
///
|
|
||||||
/// TODO(gcs-vehicle-catalog): generate this catalog from reviewed firmware
|
|
||||||
/// build manifests, then persist both this revision and a definition ID in a
|
|
||||||
/// versioned local vehicle workspace record.
|
|
||||||
const vehicleDefinitionCatalogRevision = 1;
|
|
||||||
|
|
||||||
/// Local vehicle types available in the tactical scene catalog.
|
|
||||||
const supportedVehicleDefinitions = <VehicleDefinition>[
|
|
||||||
VehicleDefinition(
|
|
||||||
id: 'kaptan.vehicle.quadcopter.local',
|
|
||||||
name: 'Quadcopter',
|
|
||||||
vehicleClass: 'Multirotor',
|
|
||||||
supportedProfile: 'Local scene unit',
|
|
||||||
media: VehicleCatalogMedia(
|
|
||||||
imageAssetPath: 'assets/vehicle_catalog/photos/quadcopter.png',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VehicleDefinition(
|
|
||||||
id: 'kaptan.vehicle.fixed-wing.local',
|
|
||||||
name: 'Fixed-wing',
|
|
||||||
vehicleClass: 'Fixed-wing',
|
|
||||||
supportedProfile: 'Local scene unit',
|
|
||||||
media: VehicleCatalogMedia(
|
|
||||||
imageAssetPath: 'assets/vehicle_catalog/photos/fixed_wing.png',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VehicleDefinition(
|
|
||||||
id: 'kaptan.vehicle.tiltrotor-vtol.local',
|
|
||||||
name: 'Tiltrotor VTOL',
|
|
||||||
vehicleClass: 'VTOL',
|
|
||||||
supportedProfile: 'Local scene unit',
|
|
||||||
media: VehicleCatalogMedia(
|
|
||||||
imageAssetPath: 'assets/vehicle_catalog/photos/tiltrotor_vtol.png',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VehicleDefinition(
|
|
||||||
id: 'kaptan.vehicle.helicopter.local',
|
|
||||||
name: 'Helicopter',
|
|
||||||
vehicleClass: 'Rotorcraft',
|
|
||||||
supportedProfile: 'Local scene unit',
|
|
||||||
media: VehicleCatalogMedia(
|
|
||||||
imageAssetPath: 'assets/vehicle_catalog/photos/helicopter.png',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VehicleDefinition(
|
|
||||||
id: 'kaptan.vehicle.rover.local',
|
|
||||||
name: 'Ground rover',
|
|
||||||
vehicleClass: 'Ground vehicle',
|
|
||||||
supportedProfile: 'Local scene unit',
|
|
||||||
media: VehicleCatalogMedia(
|
|
||||||
imageAssetPath: 'assets/vehicle_catalog/photos/rover.png',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VehicleDefinition(
|
|
||||||
id: 'kaptan.vehicle.surface-vessel.local',
|
|
||||||
name: 'Surface vessel',
|
|
||||||
vehicleClass: 'Marine vehicle',
|
|
||||||
supportedProfile: 'Local scene unit',
|
|
||||||
media: VehicleCatalogMedia(
|
|
||||||
imageAssetPath: 'assets/vehicle_catalog/photos/surface_vessel.png',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/// Bundled profile image for a local vehicle definition.
|
|
||||||
class VehicleCatalogMedia {
|
|
||||||
const VehicleCatalogMedia({required this.imageAssetPath});
|
|
||||||
|
|
||||||
final String imageAssetPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A reviewed vehicle definition that can own a Ground Suite workspace.
|
|
||||||
///
|
|
||||||
/// It is deliberately separate from platform identity, compatibility, and
|
|
||||||
/// write authority reported by a future live vehicle source.
|
|
||||||
class VehicleDefinition {
|
|
||||||
const VehicleDefinition({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.vehicleClass,
|
|
||||||
required this.supportedProfile,
|
|
||||||
required this.media,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Stable local identity for persistence and future build matching.
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String vehicleClass;
|
|
||||||
final String supportedProfile;
|
|
||||||
final VehicleCatalogMedia media;
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../fleet/domain/fleet_unit.dart';
|
|
||||||
import '../domain/vehicle_definition.dart';
|
|
||||||
|
|
||||||
final vehicleWorkspaceProvider =
|
|
||||||
NotifierProvider<VehicleWorkspaceController, VehicleWorkspaceState>(
|
|
||||||
VehicleWorkspaceController.new,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Local fleet workspace state.
|
|
||||||
///
|
|
||||||
/// TODO(gcs-fleet-storage): persist local-unit IDs and reviewed definition IDs
|
|
||||||
/// in a versioned workspace store.
|
|
||||||
class VehicleWorkspaceState {
|
|
||||||
const VehicleWorkspaceState({required this.units});
|
|
||||||
|
|
||||||
const VehicleWorkspaceState.unselected() : units = const [];
|
|
||||||
|
|
||||||
final List<FleetUnit> units;
|
|
||||||
|
|
||||||
bool get hasUnits => units.isNotEmpty;
|
|
||||||
|
|
||||||
VehicleWorkspaceState copyWith({List<FleetUnit>? units}) =>
|
|
||||||
VehicleWorkspaceState(units: units ?? this.units);
|
|
||||||
}
|
|
||||||
|
|
||||||
class VehicleWorkspaceController extends Notifier<VehicleWorkspaceState> {
|
|
||||||
static const maximumLocalUnits = 64;
|
|
||||||
static const _minimumSeparationSquared = .0016;
|
|
||||||
|
|
||||||
@override
|
|
||||||
VehicleWorkspaceState build() => const VehicleWorkspaceState.unselected();
|
|
||||||
|
|
||||||
/// Validation shared by the hangar draft and committed workspace updates.
|
|
||||||
/// It applies only to the operator-local display label, never a vehicle ID.
|
|
||||||
static bool isValidLocalDisplayName(String value) =>
|
|
||||||
_normalizeDisplayName(value) != null;
|
|
||||||
|
|
||||||
bool canPlace(LocalScenePoint localPlacement) =>
|
|
||||||
state.units.length < maximumLocalUnits &&
|
|
||||||
state.units.every((unit) {
|
|
||||||
final dx = unit.localPlacement.x - localPlacement.x;
|
|
||||||
final dy = unit.localPlacement.y - localPlacement.y;
|
|
||||||
return dx * dx + dy * dy >= _minimumSeparationSquared;
|
|
||||||
});
|
|
||||||
|
|
||||||
bool addUnit({
|
|
||||||
required VehicleDefinition definition,
|
|
||||||
required LocalScenePoint localPlacement,
|
|
||||||
String? displayName,
|
|
||||||
}) {
|
|
||||||
if (!canPlace(localPlacement)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final sequence = state.units.length + 1;
|
|
||||||
final unit = FleetUnit(
|
|
||||||
localUnitId: 'local-unit-$sequence',
|
|
||||||
displayName:
|
|
||||||
_normalizeDisplayName(displayName) ??
|
|
||||||
'${definition.name.toUpperCase()}-${sequence.toString().padLeft(2, '0')}',
|
|
||||||
definition: definition,
|
|
||||||
localPlacement: localPlacement,
|
|
||||||
);
|
|
||||||
state = VehicleWorkspaceState(units: [...state.units, unit]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a local unit at a deterministic, scattered scene position.
|
|
||||||
///
|
|
||||||
/// Until a reviewed MAVLink position binding exists, this is presentation
|
|
||||||
/// placement only: it is neither a GPS position nor received vehicle data.
|
|
||||||
bool addUnitAutomatically({
|
|
||||||
required VehicleDefinition definition,
|
|
||||||
String? displayName,
|
|
||||||
}) {
|
|
||||||
for (var attempt = 0; attempt < maximumLocalUnits; attempt++) {
|
|
||||||
final sequence = state.units.length + attempt + 1;
|
|
||||||
final placement = LocalScenePoint(
|
|
||||||
x: .14 + _fraction(sequence * .61803398875) * .72,
|
|
||||||
y: .16 + _fraction(sequence * .41421356237) * .66,
|
|
||||||
);
|
|
||||||
if (addUnit(
|
|
||||||
definition: definition,
|
|
||||||
localPlacement: placement,
|
|
||||||
displayName: displayName,
|
|
||||||
)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static double _fraction(double value) => value - value.floorToDouble();
|
|
||||||
|
|
||||||
bool renameUnit(String localUnitId, String displayName) {
|
|
||||||
final normalized = _normalizeDisplayName(displayName);
|
|
||||||
if (normalized == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var changed = false;
|
|
||||||
final units = state.units
|
|
||||||
.map((unit) {
|
|
||||||
if (unit.localUnitId != localUnitId) {
|
|
||||||
return unit;
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
return unit.copyWith(displayName: normalized);
|
|
||||||
})
|
|
||||||
.toList(growable: false);
|
|
||||||
if (!changed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
state = state.copyWith(units: units);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String? _normalizeDisplayName(String? value) {
|
|
||||||
final normalized = value?.trim();
|
|
||||||
if (normalized == null ||
|
|
||||||
normalized.isEmpty ||
|
|
||||||
normalized.runes.length > 32) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../../../../shared/widgets/surfaces.dart';
|
|
||||||
import '../../domain/vehicle_definition.dart';
|
|
||||||
|
|
||||||
/// A compact, keyboard-selectable workspace definition tile.
|
|
||||||
///
|
|
||||||
/// Only a reviewed [VehicleDefinition] reaches this widget. The radio control
|
|
||||||
/// changes local pending workspace state; it does not connect, configure, or
|
|
||||||
/// identify a vehicle.
|
|
||||||
class VehicleCatalogCard extends StatefulWidget {
|
|
||||||
const VehicleCatalogCard({
|
|
||||||
super.key,
|
|
||||||
required this.definition,
|
|
||||||
required this.selected,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
final VehicleDefinition definition;
|
|
||||||
final bool selected;
|
|
||||||
final VoidCallback onSelected;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<VehicleCatalogCard> createState() => _VehicleCatalogCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VehicleCatalogCardState extends State<VehicleCatalogCard> {
|
|
||||||
bool _showFocus = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final scheme = Theme.of(context).colorScheme;
|
|
||||||
final accent = widget.selected || _showFocus ? scheme.primary : null;
|
|
||||||
|
|
||||||
return Semantics(
|
|
||||||
button: true,
|
|
||||||
checked: widget.selected,
|
|
||||||
inMutuallyExclusiveGroup: true,
|
|
||||||
label:
|
|
||||||
'${widget.definition.name}, ${widget.definition.vehicleClass}, '
|
|
||||||
'${widget.definition.supportedProfile}, local unit type, '
|
|
||||||
'local vehicle image, not telemetry',
|
|
||||||
onTap: widget.onSelected,
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: _showFocus
|
|
||||||
? Border.all(color: scheme.primary, width: 2)
|
|
||||||
: null,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
excludeFromSemantics: true,
|
|
||||||
onFocusChange: (value) => setState(() => _showFocus = value),
|
|
||||||
onTap: widget.onSelected,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: InstrumentPanel(
|
|
||||||
accent: accent,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_VehicleImage(media: widget.definition.media),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ConsoleLabel('Local unit type'),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
widget.definition.name,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
'${widget.definition.vehicleClass} · ${widget.definition.supportedProfile}',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
widget.selected
|
|
||||||
? Icons.radio_button_checked
|
|
||||||
: Icons.radio_button_off,
|
|
||||||
size: 18,
|
|
||||||
color: widget.selected
|
|
||||||
? scheme.primary
|
|
||||||
: scheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 7),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
widget.selected
|
|
||||||
? 'Selected for workspace'
|
|
||||||
: 'Select for workspace',
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VehicleImage extends StatelessWidget {
|
|
||||||
const _VehicleImage({required this.media});
|
|
||||||
|
|
||||||
final VehicleCatalogMedia media;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(11)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 10,
|
|
||||||
child: Image.asset(
|
|
||||||
media.imageAssetPath,
|
|
||||||
key: Key('vehicle-image-${media.imageAssetPath}'),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
excludeFromSemantics: true,
|
|
||||||
errorBuilder: (context, error, stackTrace) => ColoredBox(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
||||||
child: const Center(child: Icon(Icons.image_not_supported_outlined)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import 'app/app.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
runApp(const GroundSuiteApp());
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../../core/observations/observed_event.dart';
|
|
||||||
|
|
||||||
class SeverityBadge extends StatelessWidget {
|
|
||||||
const SeverityBadge({required this.level, super.key});
|
|
||||||
|
|
||||||
final MavSeverity level;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final dark = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
final (icon, color) = switch (level.wireValue) {
|
|
||||||
0 || 1 || 2 || 3 => (
|
|
||||||
Icons.error_outline,
|
|
||||||
dark ? const Color(0xffff6b63) : const Color(0xffd92d20),
|
|
||||||
),
|
|
||||||
4 => (
|
|
||||||
Icons.warning_amber_outlined,
|
|
||||||
dark ? const Color(0xfff7b955) : const Color(0xffb54708),
|
|
||||||
),
|
|
||||||
5 || 6 => (
|
|
||||||
Icons.info_outline,
|
|
||||||
dark ? const Color(0xff70b8ff) : const Color(0xff175cd3),
|
|
||||||
),
|
|
||||||
7 => (
|
|
||||||
Icons.bug_report_outlined,
|
|
||||||
dark ? const Color(0xffa8b5ac) : const Color(0xff546258),
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
Icons.help_outline,
|
|
||||||
dark ? const Color(0xffa8b5ac) : const Color(0xff546258),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
return Semantics(
|
|
||||||
label: 'Severity: ${level.label}',
|
|
||||||
child: Icon(icon, color: color),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../../app/theme.dart';
|
|
||||||
|
|
||||||
/// A blurred surface for stable application chrome only.
|
|
||||||
///
|
|
||||||
/// Keep this out of scrollable content. Diagnostic rows and metric panels use
|
|
||||||
/// [InstrumentPanel], which has the same visual language without creating a
|
|
||||||
/// compositing layer per item.
|
|
||||||
class FrostedChrome extends StatelessWidget {
|
|
||||||
const FrostedChrome({
|
|
||||||
required this.child,
|
|
||||||
super.key,
|
|
||||||
this.borderRadius = BorderRadius.zero,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final BorderRadius borderRadius;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final highContrast = MediaQuery.maybeOf(context)?.highContrast ?? false;
|
|
||||||
final surface = _SurfaceFrame(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
chrome: true,
|
|
||||||
opaque: highContrast,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
if (highContrast) {
|
|
||||||
return surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
|
||||||
child: surface,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An opaque, high-legibility panel for operational data and event rows.
|
|
||||||
class InstrumentPanel extends StatelessWidget {
|
|
||||||
const InstrumentPanel({
|
|
||||||
required this.child,
|
|
||||||
super.key,
|
|
||||||
this.borderRadius = const BorderRadius.all(Radius.circular(12)),
|
|
||||||
this.accent,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final BorderRadius borderRadius;
|
|
||||||
final Color? accent;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => _SurfaceFrame(
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
accent: accent,
|
|
||||||
opaque: true,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SurfaceFrame extends StatelessWidget {
|
|
||||||
const _SurfaceFrame({
|
|
||||||
required this.child,
|
|
||||||
required this.borderRadius,
|
|
||||||
this.chrome = false,
|
|
||||||
this.opaque = false,
|
|
||||||
this.accent,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final BorderRadius borderRadius;
|
|
||||||
final bool chrome;
|
|
||||||
final bool opaque;
|
|
||||||
final Color? accent;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final scheme = Theme.of(context).colorScheme;
|
|
||||||
final dark = scheme.brightness == Brightness.dark;
|
|
||||||
final opacity = opaque ? 0xf6 : (dark ? 0xbf : 0xd9);
|
|
||||||
final top = dark
|
|
||||||
? Color.fromARGB(
|
|
||||||
opacity,
|
|
||||||
chrome ? 12 : 16,
|
|
||||||
chrome ? 17 : 23,
|
|
||||||
chrome ? 14 : 18,
|
|
||||||
)
|
|
||||||
: Color.fromARGB(opacity, 255, 255, 255);
|
|
||||||
final bottom = dark
|
|
||||||
? Color.fromARGB(opacity, 8, 11, 9)
|
|
||||||
: Color.fromARGB(opacity, 244, 248, 244);
|
|
||||||
final border =
|
|
||||||
accent?.withValues(alpha: 0.74) ??
|
|
||||||
scheme.outline.withValues(alpha: dark ? 0.92 : 0.82);
|
|
||||||
|
|
||||||
return DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [top, bottom],
|
|
||||||
),
|
|
||||||
border: Border.all(color: border),
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: dark ? 0.18 : 0.06),
|
|
||||||
blurRadius: 18,
|
|
||||||
offset: const Offset(0, 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The static canvas behind Ground Suite's chrome and instrument surfaces.
|
|
||||||
class AppCanvas extends StatelessWidget {
|
|
||||||
const AppCanvas({required this.child, super.key});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final dark = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
final base = dark ? const Color(0xff080b09) : const Color(0xfff3f6f3);
|
|
||||||
final primaryHaze = (dark ? accentMint : const Color(0xff087a3b))
|
|
||||||
.withValues(alpha: dark ? 0.075 : 0.05);
|
|
||||||
|
|
||||||
return ColoredBox(
|
|
||||||
color: base,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
top: -250,
|
|
||||||
right: -200,
|
|
||||||
child: IgnorePointer(child: _Haze(color: primaryHaze)),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -330,
|
|
||||||
left: -260,
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: _Haze(color: primaryHaze.withValues(alpha: 0.55)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.fill(child: child),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Haze extends StatelessWidget {
|
|
||||||
const _Haze({required this.color});
|
|
||||||
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Container(
|
|
||||||
width: 600,
|
|
||||||
height: 600,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
gradient: RadialGradient(colors: [color, Colors.transparent]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConsoleLabel extends StatelessWidget {
|
|
||||||
const ConsoleLabel(this.text, {super.key, this.color});
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Text(
|
|
||||||
text.toUpperCase(),
|
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
||||||
color: color ?? Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
letterSpacing: 1.25,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusBadge extends StatelessWidget {
|
|
||||||
const StatusBadge({required this.label, super.key, this.color});
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final scheme = Theme.of(context).colorScheme;
|
|
||||||
final tone = color ?? scheme.onSurfaceVariant;
|
|
||||||
return DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: tone.withValues(alpha: 0.11),
|
|
||||||
border: Border.all(color: tone.withValues(alpha: 0.45)),
|
|
||||||
borderRadius: BorderRadius.circular(7),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
||||||
child: ConsoleLabel(label, color: tone),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
flutter/ephemeral
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Project-level configuration.
|
|
||||||
cmake_minimum_required(VERSION 3.13)
|
|
||||||
project(runner LANGUAGES CXX)
|
|
||||||
|
|
||||||
# The name of the executable created for the application. Change this to change
|
|
||||||
# the on-disk name of your application.
|
|
||||||
set(BINARY_NAME "ground_suite")
|
|
||||||
# The unique GTK application identifier for this application. See:
|
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
|
||||||
set(APPLICATION_ID "dev.kaptan.ground_suite")
|
|
||||||
|
|
||||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
|
||||||
# versions of CMake.
|
|
||||||
cmake_policy(SET CMP0063 NEW)
|
|
||||||
|
|
||||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
|
||||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
|
||||||
|
|
||||||
# Root filesystem for cross-building.
|
|
||||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
|
||||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
|
||||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Define build configuration options.
|
|
||||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|
||||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
|
||||||
STRING "Flutter build mode" FORCE)
|
|
||||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
|
||||||
"Debug" "Profile" "Release")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Compilation settings that should be applied to most targets.
|
|
||||||
#
|
|
||||||
# Be cautious about adding new options here, as plugins use this function by
|
|
||||||
# default. In most cases, you should add new options to specific targets instead
|
|
||||||
# of modifying this function.
|
|
||||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
|
||||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
|
||||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
|
||||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
|
||||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
# Flutter library and tool build rules.
|
|
||||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
|
||||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
|
||||||
|
|
||||||
# System-level dependencies.
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
|
||||||
|
|
||||||
# Application build; see runner/CMakeLists.txt.
|
|
||||||
add_subdirectory("runner")
|
|
||||||
|
|
||||||
# Run the Flutter tool portions of the build. This must not be removed.
|
|
||||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
|
||||||
|
|
||||||
# Only the install-generated bundle's copy of the executable will launch
|
|
||||||
# correctly, since the resources must in the right relative locations. To avoid
|
|
||||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
|
||||||
# the default top-level location.
|
|
||||||
set_target_properties(${BINARY_NAME}
|
|
||||||
PROPERTIES
|
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Generated plugin build rules, which manage building the plugins and adding
|
|
||||||
# them to the application.
|
|
||||||
include(flutter/generated_plugins.cmake)
|
|
||||||
|
|
||||||
|
|
||||||
# === Installation ===
|
|
||||||
# By default, "installing" just makes a relocatable bundle in the build
|
|
||||||
# directory.
|
|
||||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
|
||||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
|
||||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Start with a clean build bundle directory every time.
|
|
||||||
install(CODE "
|
|
||||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
|
||||||
" COMPONENT Runtime)
|
|
||||||
|
|
||||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
|
||||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
|
||||||
|
|
||||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
|
||||||
COMPONENT Runtime)
|
|
||||||
|
|
||||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
|
||||||
COMPONENT Runtime)
|
|
||||||
|
|
||||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
|
||||||
COMPONENT Runtime)
|
|
||||||
|
|
||||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
|
||||||
install(FILES "${bundled_library}"
|
|
||||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
|
||||||
COMPONENT Runtime)
|
|
||||||
endforeach(bundled_library)
|
|
||||||
|
|
||||||
# Copy the native assets provided by the build.dart from all packages.
|
|
||||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
|
||||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
|
||||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
|
||||||
COMPONENT Runtime)
|
|
||||||
|
|
||||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
|
||||||
# from a previous install.
|
|
||||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
|
||||||
install(CODE "
|
|
||||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
|
||||||
" COMPONENT Runtime)
|
|
||||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
|
||||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
|
||||||
|
|
||||||
# Install the AOT library on non-Debug builds only.
|
|
||||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
|
||||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
|
||||||
COMPONENT Runtime)
|
|
||||||
endif()
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# This file controls Flutter-level build steps. It should not be edited.
|
|
||||||
cmake_minimum_required(VERSION 3.10)
|
|
||||||
|
|
||||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
|
||||||
|
|
||||||
# Configuration provided via flutter tool.
|
|
||||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
|
||||||
|
|
||||||
# TODO: Move the rest of this into files in ephemeral. See
|
|
||||||
# https://github.com/flutter/flutter/issues/57146.
|
|
||||||
|
|
||||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
|
||||||
# which isn't available in 3.10.
|
|
||||||
function(list_prepend LIST_NAME PREFIX)
|
|
||||||
set(NEW_LIST "")
|
|
||||||
foreach(element ${${LIST_NAME}})
|
|
||||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
|
||||||
endforeach(element)
|
|
||||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
# === Flutter Library ===
|
|
||||||
# System-level dependencies.
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
|
||||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
|
||||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
|
||||||
|
|
||||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
|
||||||
|
|
||||||
# Published to parent scope for install step.
|
|
||||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
|
||||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
|
||||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
|
||||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
|
||||||
"fl_basic_message_channel.h"
|
|
||||||
"fl_binary_codec.h"
|
|
||||||
"fl_binary_messenger.h"
|
|
||||||
"fl_dart_project.h"
|
|
||||||
"fl_engine.h"
|
|
||||||
"fl_json_message_codec.h"
|
|
||||||
"fl_json_method_codec.h"
|
|
||||||
"fl_message_codec.h"
|
|
||||||
"fl_method_call.h"
|
|
||||||
"fl_method_channel.h"
|
|
||||||
"fl_method_codec.h"
|
|
||||||
"fl_method_response.h"
|
|
||||||
"fl_plugin_registrar.h"
|
|
||||||
"fl_plugin_registry.h"
|
|
||||||
"fl_standard_message_codec.h"
|
|
||||||
"fl_standard_method_codec.h"
|
|
||||||
"fl_string_codec.h"
|
|
||||||
"fl_value.h"
|
|
||||||
"fl_view.h"
|
|
||||||
"flutter_linux.h"
|
|
||||||
)
|
|
||||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
|
||||||
add_library(flutter INTERFACE)
|
|
||||||
target_include_directories(flutter INTERFACE
|
|
||||||
"${EPHEMERAL_DIR}"
|
|
||||||
)
|
|
||||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
|
||||||
target_link_libraries(flutter INTERFACE
|
|
||||||
PkgConfig::GTK
|
|
||||||
PkgConfig::GLIB
|
|
||||||
PkgConfig::GIO
|
|
||||||
)
|
|
||||||
add_dependencies(flutter flutter_assemble)
|
|
||||||
|
|
||||||
# === Flutter tool backend ===
|
|
||||||
# _phony_ is a non-existent file to force this command to run every time,
|
|
||||||
# since currently there's no way to get a full input/output list from the
|
|
||||||
# flutter tool.
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E env
|
|
||||||
${FLUTTER_TOOL_ENVIRONMENT}
|
|
||||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
|
||||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
add_custom_target(flutter_assemble DEPENDS
|
|
||||||
"${FLUTTER_LIBRARY}"
|
|
||||||
${FLUTTER_LIBRARY_HEADERS}
|
|
||||||
)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
|
||||||
|
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
|
|
||||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
#define GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
|
|
||||||
#include <flutter_linux/flutter_linux.h>
|
|
||||||
|
|
||||||
// Registers Flutter plugins.
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry);
|
|
||||||
|
|
||||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#
|
|
||||||
# Generated file, do not edit.
|
|
||||||
#
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
|
||||||
)
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
|
||||||
jni
|
|
||||||
)
|
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
|
||||||
|
|
||||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
|
||||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
|
||||||
endforeach(plugin)
|
|
||||||
|
|
||||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
|
||||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
|
||||||
endforeach(ffi_plugin)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.13)
|
|
||||||
project(runner LANGUAGES CXX)
|
|
||||||
|
|
||||||
# Define the application target. To change its name, change BINARY_NAME in the
|
|
||||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
|
||||||
# work.
|
|
||||||
#
|
|
||||||
# Any new source files that you add to the application should be added here.
|
|
||||||
add_executable(${BINARY_NAME}
|
|
||||||
"main.cc"
|
|
||||||
"my_application.cc"
|
|
||||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Apply the standard set of build settings. This can be removed for applications
|
|
||||||
# that need different build settings.
|
|
||||||
apply_standard_settings(${BINARY_NAME})
|
|
||||||
|
|
||||||
# Add preprocessor definitions for the application ID.
|
|
||||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
|
||||||
|
|
||||||
# Add dependency libraries. Add any application-specific dependencies here.
|
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
|
||||||
|
|
||||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#include "my_application.h"
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
g_autoptr(MyApplication) app = my_application_new();
|
|
||||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#include "my_application.h"
|
|
||||||
|
|
||||||
#include <flutter_linux/flutter_linux.h>
|
|
||||||
#ifdef GDK_WINDOWING_X11
|
|
||||||
#include <gdk/gdkx.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
|
||||||
|
|
||||||
struct _MyApplication {
|
|
||||||
GtkApplication parent_instance;
|
|
||||||
char** dart_entrypoint_arguments;
|
|
||||||
};
|
|
||||||
|
|
||||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
|
||||||
|
|
||||||
// Called when first Flutter frame received.
|
|
||||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
|
||||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::activate.
|
|
||||||
static void my_application_activate(GApplication* application) {
|
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
|
||||||
GtkWindow* window =
|
|
||||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
|
||||||
|
|
||||||
// Use a header bar when running in GNOME as this is the common style used
|
|
||||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
|
||||||
// desktop).
|
|
||||||
// If running on X and not using GNOME then just use a traditional title bar
|
|
||||||
// in case the window manager does more exotic layout, e.g. tiling.
|
|
||||||
// If running on Wayland assume the header bar will work (may need changing
|
|
||||||
// if future cases occur).
|
|
||||||
gboolean use_header_bar = TRUE;
|
|
||||||
#ifdef GDK_WINDOWING_X11
|
|
||||||
GdkScreen* screen = gtk_window_get_screen(window);
|
|
||||||
if (GDK_IS_X11_SCREEN(screen)) {
|
|
||||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
|
||||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
|
||||||
use_header_bar = FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (use_header_bar) {
|
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
|
||||||
gtk_header_bar_set_title(header_bar, "Ground Suite");
|
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
|
||||||
} else {
|
|
||||||
gtk_window_set_title(window, "Ground Suite");
|
|
||||||
}
|
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
|
||||||
|
|
||||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
|
||||||
fl_dart_project_set_dart_entrypoint_arguments(
|
|
||||||
project, self->dart_entrypoint_arguments);
|
|
||||||
|
|
||||||
FlView* view = fl_view_new(project);
|
|
||||||
GdkRGBA background_color;
|
|
||||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
|
||||||
// for transparent.
|
|
||||||
gdk_rgba_parse(&background_color, "#000000");
|
|
||||||
fl_view_set_background_color(view, &background_color);
|
|
||||||
gtk_widget_show(GTK_WIDGET(view));
|
|
||||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
|
||||||
|
|
||||||
// Show the window when Flutter renders.
|
|
||||||
// Requires the view to be realized so we can start rendering.
|
|
||||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
|
||||||
self);
|
|
||||||
gtk_widget_realize(GTK_WIDGET(view));
|
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
|
||||||
|
|
||||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::local_command_line.
|
|
||||||
static gboolean my_application_local_command_line(GApplication* application,
|
|
||||||
gchar*** arguments,
|
|
||||||
int* exit_status) {
|
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
|
||||||
// Strip out the first argument as it is the binary name.
|
|
||||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
|
||||||
|
|
||||||
g_autoptr(GError) error = nullptr;
|
|
||||||
if (!g_application_register(application, nullptr, &error)) {
|
|
||||||
g_warning("Failed to register: %s", error->message);
|
|
||||||
*exit_status = 1;
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_application_activate(application);
|
|
||||||
*exit_status = 0;
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::startup.
|
|
||||||
static void my_application_startup(GApplication* application) {
|
|
||||||
// MyApplication* self = MY_APPLICATION(object);
|
|
||||||
|
|
||||||
// Perform any actions required at application startup.
|
|
||||||
|
|
||||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::shutdown.
|
|
||||||
static void my_application_shutdown(GApplication* application) {
|
|
||||||
// MyApplication* self = MY_APPLICATION(object);
|
|
||||||
|
|
||||||
// Perform any actions required at application shutdown.
|
|
||||||
|
|
||||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GObject::dispose.
|
|
||||||
static void my_application_dispose(GObject* object) {
|
|
||||||
MyApplication* self = MY_APPLICATION(object);
|
|
||||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
|
||||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void my_application_class_init(MyApplicationClass* klass) {
|
|
||||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
|
||||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
|
||||||
my_application_local_command_line;
|
|
||||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
|
||||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
|
||||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void my_application_init(MyApplication* self) {}
|
|
||||||
|
|
||||||
MyApplication* my_application_new() {
|
|
||||||
// Set the program name to the application ID, which helps various systems
|
|
||||||
// like GTK and desktop environments map this running application to its
|
|
||||||
// corresponding .desktop file. This ensures better integration by allowing
|
|
||||||
// the application to be recognized beyond its binary name.
|
|
||||||
g_set_prgname(APPLICATION_ID);
|
|
||||||
|
|
||||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
|
||||||
"application-id", APPLICATION_ID, "flags",
|
|
||||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
|
||||||
#define FLUTTER_MY_APPLICATION_H_
|
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
|
||||||
|
|
||||||
G_DECLARE_FINAL_TYPE(MyApplication,
|
|
||||||
my_application,
|
|
||||||
MY,
|
|
||||||
APPLICATION,
|
|
||||||
GtkApplication)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* my_application_new:
|
|
||||||
*
|
|
||||||
* Creates a new Flutter-based application.
|
|
||||||
*
|
|
||||||
* Returns: a new #MyApplication.
|
|
||||||
*/
|
|
||||||
MyApplication* my_application_new();
|
|
||||||
|
|
||||||
#endif // FLUTTER_MY_APPLICATION_H_
|
|
||||||
@@ -1,677 +0,0 @@
|
|||||||
# Generated by pub
|
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
|
||||||
packages:
|
|
||||||
_fe_analyzer_shared:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: _fe_analyzer_shared
|
|
||||||
sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "99.0.0"
|
|
||||||
analyzer:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: analyzer
|
|
||||||
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "12.1.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"
|
|
||||||
boolean_selector:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: boolean_selector
|
|
||||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.2"
|
|
||||||
characters:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: characters
|
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.1"
|
|
||||||
cli_config:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: cli_config
|
|
||||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0"
|
|
||||||
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"
|
|
||||||
coverage:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: coverage
|
|
||||||
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.15.1"
|
|
||||||
crypto:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: crypto
|
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.7"
|
|
||||||
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_lints:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: flutter_lints
|
|
||||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.0.0"
|
|
||||||
flutter_riverpod:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_riverpod
|
|
||||||
sha256: "56e81e662e6e54e59b231da57e90ac0d06ac67d8e3f2273c129a37ae6282b40c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.4.2"
|
|
||||||
flutter_test:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
frontend_server_client:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: frontend_server_client
|
|
||||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.0"
|
|
||||||
glob:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: glob
|
|
||||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
google_fonts:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: google_fonts
|
|
||||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.3.3"
|
|
||||||
hooks:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: hooks
|
|
||||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
http:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http
|
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.6.0"
|
|
||||||
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"
|
|
||||||
leak_tracker:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker
|
|
||||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "11.0.2"
|
|
||||||
leak_tracker_flutter_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_flutter_testing
|
|
||||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.10"
|
|
||||||
leak_tracker_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_testing
|
|
||||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.2"
|
|
||||||
lints:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: lints
|
|
||||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.1.0"
|
|
||||||
listen:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: listen
|
|
||||||
sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
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: transitive
|
|
||||||
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"
|
|
||||||
node_preamble:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: node_preamble
|
|
||||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
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: transitive
|
|
||||||
description:
|
|
||||||
name: path
|
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.1"
|
|
||||||
path_provider:
|
|
||||||
dependency: transitive
|
|
||||||
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"
|
|
||||||
pub_semver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pub_semver
|
|
||||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.0"
|
|
||||||
record_use:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: record_use
|
|
||||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.6.0"
|
|
||||||
riverpod:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: riverpod
|
|
||||||
sha256: "84351b3472a447f7b89f961f95c73287009d58c59d8cc1267425d995f78e50f1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.4.2"
|
|
||||||
shelf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf
|
|
||||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.2"
|
|
||||||
shelf_packages_handler:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_packages_handler
|
|
||||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.2"
|
|
||||||
shelf_static:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_static
|
|
||||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.3"
|
|
||||||
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_map_stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_map_stack_trace
|
|
||||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.2"
|
|
||||||
source_maps:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_maps
|
|
||||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.13"
|
|
||||||
source_span:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_span
|
|
||||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.10.2"
|
|
||||||
stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stack_trace
|
|
||||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.12.1"
|
|
||||||
state_notifier:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: state_notifier
|
|
||||||
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
stream_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_channel
|
|
||||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
string_scanner:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: string_scanner
|
|
||||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.1"
|
|
||||||
term_glyph:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: term_glyph
|
|
||||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.2"
|
|
||||||
test:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: test
|
|
||||||
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.31.0"
|
|
||||||
test_api:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: test_api
|
|
||||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.11"
|
|
||||||
test_core:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: test_core
|
|
||||||
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.6.17"
|
|
||||||
typed_data:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: typed_data
|
|
||||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
uuid:
|
|
||||||
dependency: transitive
|
|
||||||
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"
|
|
||||||
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"
|
|
||||||
webkit_inspection_protocol:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: webkit_inspection_protocol
|
|
||||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.1"
|
|
||||||
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.12.2 <4.0.0"
|
|
||||||
flutter: ">=3.38.4"
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
name: ground_suite
|
|
||||||
description: "Kaptan's desktop ground-control workspace."
|
|
||||||
# The following line prevents the package from being accidentally published to
|
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
|
||||||
version: 0.1.0+1
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ^3.12.2
|
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
|
||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
|
||||||
# versions available, run `flutter pub outdated`.
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
flutter_riverpod: ^3.4.2
|
|
||||||
google_fonts: ^6.3.2
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
|
||||||
# package. See that file for information about deactivating specific lint
|
|
||||||
# rules and activating additional ones.
|
|
||||||
flutter_lints: ^6.0.0
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
|
||||||
# included with your application, so that you can use the icons in
|
|
||||||
# the material Icons class.
|
|
||||||
uses-material-design: true
|
|
||||||
|
|
||||||
assets:
|
|
||||||
- assets/test.png
|
|
||||||
- assets/vehicle_catalog/
|
|
||||||
- assets/vehicle_catalog/photos/
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/to/asset-from-package
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/to/font-from-package
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ground_suite/core/observations/observed_event.dart';
|
|
||||||
import 'package:ground_suite/core/observations/vehicle_data_update.dart';
|
|
||||||
import 'package:ground_suite/core/transport/replay_vehicle_data_port.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('replay covers identity, telemetry, log, error, and shutdown', () async {
|
|
||||||
const port = ReplayVehicleDataPort();
|
|
||||||
|
|
||||||
final events = [
|
|
||||||
for (final update in await port.observe().toList())
|
|
||||||
if (update case ObservationReceived(:final event)) event,
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(events.whereType<HeartbeatEvent>(), hasLength(1));
|
|
||||||
expect(events.whereType<TelemetryEvent>(), hasLength(2));
|
|
||||||
final logs = events.whereType<LogEvent>().toList();
|
|
||||||
final errors = events.whereType<ErrorEvent>().toList();
|
|
||||||
expect(logs, hasLength(2));
|
|
||||||
expect(errors, hasLength(1));
|
|
||||||
expect(events.whereType<ShutdownEvent>(), hasLength(1));
|
|
||||||
expect(logs.first.isCoalesced, isTrue);
|
|
||||||
expect(errors.single.isTruncated, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('unknown wire enum values remain visible', () {
|
|
||||||
expect(const MavSeverity(99).label, 'Unknown (99)');
|
|
||||||
expect(const KaptanShutdownReason(99).label, 'Unknown (99)');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ground_suite/features/command/presentation/tactical_command_page.dart';
|
|
||||||
import 'package:ground_suite/features/command/presentation/tactical_controller.dart';
|
|
||||||
import 'package:ground_suite/features/fleet/domain/fleet_unit.dart';
|
|
||||||
import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
ProviderContainer container() {
|
|
||||||
final value = ProviderContainer();
|
|
||||||
addTearDown(value.dispose);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
test('selection and cursor modes have one deterministic owner', () {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
|
|
||||||
controller.selectUnit('DEMO-1022');
|
|
||||||
expect(scope.read(tacticalControllerProvider).selectedUnitIds, {
|
|
||||||
'DEMO-1022',
|
|
||||||
});
|
|
||||||
|
|
||||||
controller.selectUnit('DEMO-1023', toggle: true);
|
|
||||||
expect(scope.read(tacticalControllerProvider).selectedUnitIds, {
|
|
||||||
'DEMO-1022',
|
|
||||||
'DEMO-1023',
|
|
||||||
});
|
|
||||||
|
|
||||||
controller.beginPencil();
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).mode,
|
|
||||||
isA<TacticalPencilMode>(),
|
|
||||||
);
|
|
||||||
expect(scope.read(tacticalControllerProvider).selectedUnitIds, isEmpty);
|
|
||||||
|
|
||||||
controller.showUnitHangar();
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).panel,
|
|
||||||
TacticalPanel.unitHangar,
|
|
||||||
);
|
|
||||||
expect(scope.read(tacticalControllerProvider).unitDraft, isNull);
|
|
||||||
|
|
||||||
final definition = supportedVehicleDefinitions.first;
|
|
||||||
controller.chooseUnitDefinition(definition, displayName: 'QUADCOPTER-01');
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).unitDraft?.definition,
|
|
||||||
definition,
|
|
||||||
);
|
|
||||||
controller.beginUnitSetup();
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).panel,
|
|
||||||
TacticalPanel.unitSetup,
|
|
||||||
);
|
|
||||||
controller.completeUnitPlacement('local-unit-1');
|
|
||||||
expect(scope.read(tacticalControllerProvider).selectedUnitIds, {
|
|
||||||
'local-unit-1',
|
|
||||||
});
|
|
||||||
expect(scope.read(tacticalControllerProvider).unitDraft, isNull);
|
|
||||||
controller.showUnitSetupForSelectedLocalUnit('local-unit-1');
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).panel,
|
|
||||||
TacticalPanel.unitSetup,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'hangar drafts preserve the selected type and do not mutate map state',
|
|
||||||
() {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
final before = scope.read(tacticalControllerProvider);
|
|
||||||
|
|
||||||
controller.showUnitHangar();
|
|
||||||
controller.chooseUnitDefinition(
|
|
||||||
supportedVehicleDefinitions[1],
|
|
||||||
displayName: 'Fixed wing scout',
|
|
||||||
);
|
|
||||||
controller.beginUnitSetup();
|
|
||||||
controller.updateUnitDraftName('Fixed wing north');
|
|
||||||
|
|
||||||
final editing = scope.read(tacticalControllerProvider);
|
|
||||||
expect(
|
|
||||||
identical(editing.selectedUnitIds, before.selectedUnitIds),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
expect(identical(editing.drawings, before.drawings), isTrue);
|
|
||||||
expect(identical(editing.markers, before.markers), isTrue);
|
|
||||||
expect(editing.panel, TacticalPanel.unitSetup);
|
|
||||||
|
|
||||||
controller.returnToUnitHangar();
|
|
||||||
final returned = scope.read(tacticalControllerProvider);
|
|
||||||
expect(returned.panel, TacticalPanel.unitHangar);
|
|
||||||
expect(returned.unitDraft?.definition, supportedVehicleDefinitions[1]);
|
|
||||||
expect(returned.unitDraft?.displayName, 'Fixed wing north');
|
|
||||||
|
|
||||||
controller.showUnitSetupForSelectedLocalUnit('local-unit-1');
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).panel,
|
|
||||||
TacticalPanel.unitHangar,
|
|
||||||
reason:
|
|
||||||
'A catalog draft cannot produce an unrelated blank setup panel.',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'editing a hangar draft does not notify the tactical render projection',
|
|
||||||
() async {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
scope.read(tacticalSceneRenderStateProvider);
|
|
||||||
var renderNotifications = 0;
|
|
||||||
final subscription = scope.listen<TacticalSceneRenderState>(
|
|
||||||
tacticalSceneRenderStateProvider,
|
|
||||||
(previous, next) => renderNotifications += 1,
|
|
||||||
fireImmediately: true,
|
|
||||||
);
|
|
||||||
addTearDown(subscription.close);
|
|
||||||
renderNotifications = 0;
|
|
||||||
|
|
||||||
controller.showUnitHangar();
|
|
||||||
await scope.pump();
|
|
||||||
controller.chooseUnitDefinition(
|
|
||||||
supportedVehicleDefinitions.first,
|
|
||||||
displayName: 'Quad local',
|
|
||||||
);
|
|
||||||
await scope.pump();
|
|
||||||
controller.updateUnitDraftName('Quad north');
|
|
||||||
await scope.pump();
|
|
||||||
expect(
|
|
||||||
renderNotifications,
|
|
||||||
1,
|
|
||||||
reason: 'Opening the hangar changes its panel visibility.',
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.updateUnitDraftName('Quad north one');
|
|
||||||
await scope.pump();
|
|
||||||
expect(
|
|
||||||
renderNotifications,
|
|
||||||
1,
|
|
||||||
reason: 'Typing only updates the isolated local setup draft.',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'direct pencil strokes are session state and require complete geometry',
|
|
||||||
() {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
|
|
||||||
controller.beginPencil();
|
|
||||||
expect(
|
|
||||||
controller.commitPencilStroke(const [LocalScenePoint(x: .1, y: .2)]),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
controller.commitPencilStroke(const [
|
|
||||||
LocalScenePoint(x: .1, y: .2),
|
|
||||||
LocalScenePoint(x: .3, y: .4),
|
|
||||||
]),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
|
|
||||||
final state = scope.read(tacticalControllerProvider);
|
|
||||||
expect(state.mode, isA<TacticalPencilMode>());
|
|
||||||
expect(state.drawings, hasLength(1));
|
|
||||||
expect(state.drawings.single.thickness, 3);
|
|
||||||
|
|
||||||
controller.clearSceneObjects();
|
|
||||||
expect(scope.read(tacticalControllerProvider).drawings, isEmpty);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test('view toggles remain separate from selection and pencil tools', () {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
|
|
||||||
controller.beginPencil();
|
|
||||||
controller.toggleOverlay(TacticalMapOverlay.drones, false);
|
|
||||||
|
|
||||||
final state = scope.read(tacticalControllerProvider);
|
|
||||||
expect(state.mode, isA<TacticalPencilMode>());
|
|
||||||
expect(state.overlays, isNot(contains(TacticalMapOverlay.drones)));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fleet panel is separate from a local map mode', () {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
|
|
||||||
controller.showFleetRoster();
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).panel,
|
|
||||||
TacticalPanel.fleetRoster,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).mode,
|
|
||||||
isA<TacticalBrowseMode>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.beginPencil();
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).mode,
|
|
||||||
isA<TacticalPencilMode>(),
|
|
||||||
);
|
|
||||||
controller.closePanel();
|
|
||||||
expect(scope.read(tacticalControllerProvider).panel, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('drag selection is local only', () {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
controller.selectUnits(['DEMO-1021', 'DEMO-1023']);
|
|
||||||
final state = scope.read(tacticalControllerProvider);
|
|
||||||
expect(state.selectedUnitIds, {'DEMO-1021', 'DEMO-1023'});
|
|
||||||
expect(state.mode, isA<TacticalBrowseMode>());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('direct strokes are bounded before local scene commit', () {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
controller.beginPencil();
|
|
||||||
|
|
||||||
final stroke = [
|
|
||||||
for (var index = 0; index < 256; index++)
|
|
||||||
LocalScenePoint(x: index / 255, y: index.isEven ? .4 : .6),
|
|
||||||
];
|
|
||||||
expect(controller.commitPencilStroke(stroke), isTrue);
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).drawings.single.points.length,
|
|
||||||
lessThanOrEqualTo(256),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'pencil style is retained for the current cursor and committed stroke',
|
|
||||||
() {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
controller.beginPencil();
|
|
||||||
controller.configurePencil(
|
|
||||||
hue: 282,
|
|
||||||
saturation: .22,
|
|
||||||
brightness: .38,
|
|
||||||
thickness: 8,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
controller.commitPencilStroke(const [
|
|
||||||
LocalScenePoint(x: .2, y: .2),
|
|
||||||
LocalScenePoint(x: .4, y: .4),
|
|
||||||
]),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
|
|
||||||
final state = scope.read(tacticalControllerProvider);
|
|
||||||
expect(state.mode, isA<TacticalPencilMode>());
|
|
||||||
final mode = state.mode as TacticalPencilMode;
|
|
||||||
expect(mode.hue, 282);
|
|
||||||
expect(mode.saturation, .22);
|
|
||||||
expect(mode.brightness, .38);
|
|
||||||
expect(mode.thickness, 8);
|
|
||||||
expect(state.drawings.single.hue, 282);
|
|
||||||
expect(state.drawings.single.saturation, .22);
|
|
||||||
expect(state.drawings.single.brightness, .38);
|
|
||||||
expect(state.drawings.single.thickness, 8);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test('marker names and colors are independent from pencil strokes', () {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
|
|
||||||
controller.beginPencil();
|
|
||||||
controller.configurePencil(hue: 282, thickness: 8);
|
|
||||||
controller.beginMarker();
|
|
||||||
controller.configureMarker(hue: 26, saturation: .41, brightness: .56);
|
|
||||||
controller.chooseMarkerPoint(const LocalScenePoint(x: .4, y: .6));
|
|
||||||
|
|
||||||
expect(controller.commitMarkerName('Landing review'), isTrue);
|
|
||||||
final state = scope.read(tacticalControllerProvider);
|
|
||||||
expect(state.markers, hasLength(1));
|
|
||||||
expect(state.markers.single.name, 'Landing review');
|
|
||||||
expect(state.markers.single.hue, 26);
|
|
||||||
expect(state.markers.single.saturation, .41);
|
|
||||||
expect(state.markers.single.brightness, .56);
|
|
||||||
expect((state.mode as TacticalMarkerMode).hue, 26);
|
|
||||||
expect(state.drawings, isEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'blank marker labels and pending marker cancellation create no marker',
|
|
||||||
() {
|
|
||||||
final scope = container();
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
controller.beginMarker();
|
|
||||||
controller.chooseMarkerPoint(const LocalScenePoint(x: .2, y: .2));
|
|
||||||
|
|
||||||
expect(controller.commitMarkerName(' '), isFalse);
|
|
||||||
expect(controller.cancelActiveTool(), isFalse);
|
|
||||||
expect(scope.read(tacticalControllerProvider).markers, isEmpty);
|
|
||||||
expect(controller.cancelActiveTool(), isTrue);
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).mode,
|
|
||||||
isA<TacticalBrowseMode>(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ground_suite/core/observations/observed_event.dart';
|
|
||||||
import 'package:ground_suite/core/observations/vehicle_data_port.dart';
|
|
||||||
import 'package:ground_suite/core/observations/vehicle_data_update.dart';
|
|
||||||
import 'package:ground_suite/features/observability/presentation/observability_controller.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('coalesces rapid critical accessibility announcements', () async {
|
|
||||||
final port = _ControlledDataPort();
|
|
||||||
final container = ProviderContainer(
|
|
||||||
overrides: [vehicleDataPortProvider.overrideWithValue(port)],
|
|
||||||
);
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
container.read(observabilityControllerProvider);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
port.events.add(
|
|
||||||
ObservationReceived(
|
|
||||||
ErrorEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10),
|
|
||||||
path: 'kaptan_drivers::lidar_lite_v3::error::Error::TransferFault',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
container.read(observabilityControllerProvider).accessibilityAnnouncement,
|
|
||||||
'Error received: kaptan_drivers::lidar_lite_v3::error::Error::TransferFault',
|
|
||||||
);
|
|
||||||
|
|
||||||
port.events.add(
|
|
||||||
ObservationReceived(
|
|
||||||
ShutdownEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 0, 0, 1),
|
|
||||||
reason: KaptanShutdownReason.safety,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
final state = container.read(observabilityControllerProvider);
|
|
||||||
expect(state.accessibilityAnnouncement, isNull);
|
|
||||||
expect(state.coalescedAlertCount, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'retains expected parser and semantic drops without ending observation',
|
|
||||||
() async {
|
|
||||||
final port = _ControlledDataPort();
|
|
||||||
final container = ProviderContainer(
|
|
||||||
overrides: [vehicleDataPortProvider.overrideWithValue(port)],
|
|
||||||
);
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
container.read(observabilityControllerProvider);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
port.events.add(
|
|
||||||
const ObservationDropped(kind: ObservationDropKind.parser, count: 2),
|
|
||||||
);
|
|
||||||
port.events.add(
|
|
||||||
const ObservationDropped(kind: ObservationDropKind.semantic),
|
|
||||||
);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
final state = container.read(observabilityControllerProvider);
|
|
||||||
expect(state.status, ObservationStatus.receiving);
|
|
||||||
expect(state.parserDropCount, 2);
|
|
||||||
expect(state.semanticDropCount, 1);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test('treats a transport stream failure as a disconnected link', () async {
|
|
||||||
final port = _ControlledDataPort();
|
|
||||||
final container = ProviderContainer(
|
|
||||||
overrides: [vehicleDataPortProvider.overrideWithValue(port)],
|
|
||||||
);
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
container.read(observabilityControllerProvider);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
port.events.addError(StateError('serial endpoint closed'));
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
final state = container.read(observabilityControllerProvider);
|
|
||||||
expect(state.status, ObservationStatus.disconnected);
|
|
||||||
expect(state.parserDropCount, 0);
|
|
||||||
expect(state.semanticDropCount, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'bounds diagnostics while telemetry stays outside the timeline',
|
|
||||||
() async {
|
|
||||||
final port = _ControlledDataPort();
|
|
||||||
final container = ProviderContainer(
|
|
||||||
overrides: [vehicleDataPortProvider.overrideWithValue(port)],
|
|
||||||
);
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
container.read(observabilityControllerProvider);
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
|
|
||||||
port.events.add(
|
|
||||||
ObservationReceived(
|
|
||||||
ErrorEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10),
|
|
||||||
path: 'kaptan_drivers::lidar_lite_v3::error::Error::TransferFault',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
port.events.add(
|
|
||||||
ObservationReceived(
|
|
||||||
HeartbeatEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 0, 0, 1),
|
|
||||||
systemId: 7,
|
|
||||||
componentId: 42,
|
|
||||||
armed: false,
|
|
||||||
vehicleState: 'Standby',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
for (var index = 0; index < 501; index++) {
|
|
||||||
port.events.add(
|
|
||||||
ObservationReceived(
|
|
||||||
TelemetryEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 0, 0, index % 60),
|
|
||||||
kind: TelemetryKind.rawImu,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (var index = 0; index < 500; index++) {
|
|
||||||
port.events.add(
|
|
||||||
ObservationReceived(
|
|
||||||
LogEvent(
|
|
||||||
receivedAt: DateTime.utc(2026, 8, 10, 0, 1, index % 60),
|
|
||||||
level: MavSeverity.info,
|
|
||||||
message: 'Diagnostic $index',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final state = container.read(observabilityControllerProvider);
|
|
||||||
expect(state.events, hasLength(500));
|
|
||||||
expect(state.localTimelineEvictionCount, 1);
|
|
||||||
expect(state.events.whereType<ErrorEvent>(), hasLength(1));
|
|
||||||
expect(state.lastHeartbeat?.systemId, 7);
|
|
||||||
expect(state.observedTelemetryKinds, contains(TelemetryKind.rawImu));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ControlledDataPort implements VehicleDataPort {
|
|
||||||
final events = StreamController<VehicleDataUpdate>(sync: true);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isReplay => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => 'Test transport';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() => events.close();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<VehicleDataUpdate> observe() => events.stream;
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import 'dart:ui' show CheckedState;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart';
|
|
||||||
import 'package:ground_suite/features/vehicle_workspace/presentation/widgets/vehicle_catalog_card.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('every local vehicle type has a clear bundled profile image', () {
|
|
||||||
expect(supportedVehicleDefinitions, hasLength(6));
|
|
||||||
expect(
|
|
||||||
supportedVehicleDefinitions.map((definition) => definition.name),
|
|
||||||
containsAll(<String>[
|
|
||||||
'Quadcopter',
|
|
||||||
'Fixed-wing',
|
|
||||||
'Tiltrotor VTOL',
|
|
||||||
'Helicopter',
|
|
||||||
'Ground rover',
|
|
||||||
'Surface vessel',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
for (final definition in supportedVehicleDefinitions) {
|
|
||||||
expect(definition.id, isNotEmpty);
|
|
||||||
expect(definition.name, isNotEmpty);
|
|
||||||
expect(definition.media.imageAssetPath, isNotEmpty);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('catalog card is a single keyboard-activatable radio control', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
var selections = 0;
|
|
||||||
final definition = supportedVehicleDefinitions.first;
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 760,
|
|
||||||
child: VehicleCatalogCard(
|
|
||||||
definition: definition,
|
|
||||||
selected: false,
|
|
||||||
onSelected: () => selections += 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final card = find.byType(VehicleCatalogCard);
|
|
||||||
final semantics = tester.getSemantics(card);
|
|
||||||
expect(semantics.flagsCollection.isChecked, CheckedState.isFalse);
|
|
||||||
expect(semantics.flagsCollection.isInMutuallyExclusiveGroup, isTrue);
|
|
||||||
expect(semantics.label, contains('local vehicle image, not telemetry'));
|
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
|
||||||
await tester.pump();
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
||||||
await tester.pump();
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(selections, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('definition images are bundled locally', (tester) async {
|
|
||||||
for (final definition in supportedVehicleDefinitions) {
|
|
||||||
final image = await rootBundle.load(definition.media.imageAssetPath);
|
|
||||||
expect(image.lengthInBytes, greaterThan(0));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ground_suite/features/fleet/domain/fleet_unit.dart';
|
|
||||||
import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart';
|
|
||||||
import 'package:ground_suite/features/vehicle_workspace/presentation/vehicle_workspace_controller.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('a reviewed definition creates a selected local fleet unit', () {
|
|
||||||
final container = ProviderContainer();
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
|
||||||
expect(container.read(vehicleWorkspaceProvider).hasUnits, isFalse);
|
|
||||||
|
|
||||||
final definition = supportedVehicleDefinitions.first;
|
|
||||||
expect(
|
|
||||||
controller.addUnit(
|
|
||||||
definition: definition,
|
|
||||||
localPlacement: const LocalScenePoint(x: .35, y: .45),
|
|
||||||
displayName: 'North survey',
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
expect(container.read(vehicleWorkspaceProvider).hasUnits, isTrue);
|
|
||||||
expect(
|
|
||||||
container.read(vehicleWorkspaceProvider).units.single.definition,
|
|
||||||
definition,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
container.read(vehicleWorkspaceProvider).units.single.localUnitId,
|
|
||||||
'local-unit-1',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
container.read(vehicleWorkspaceProvider).units.single.displayName,
|
|
||||||
'North survey',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
controller.addUnit(
|
|
||||||
definition: definition,
|
|
||||||
localPlacement: const LocalScenePoint(x: .72, y: .45),
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
expect(container.read(vehicleWorkspaceProvider).units, hasLength(2));
|
|
||||||
expect(
|
|
||||||
container.read(vehicleWorkspaceProvider).units.last.localUnitId,
|
|
||||||
'local-unit-2',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'rejects overlapping local placements and keeps a bounded workspace',
|
|
||||||
() {
|
|
||||||
final container = ProviderContainer();
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
|
||||||
final definition = supportedVehicleDefinitions.first;
|
|
||||||
const first = LocalScenePoint(x: .2, y: .2);
|
|
||||||
expect(
|
|
||||||
controller.addUnit(definition: definition, localPlacement: first),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
controller.addUnit(definition: definition, localPlacement: first),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
controller.addUnit(
|
|
||||||
definition: definition,
|
|
||||||
localPlacement: const LocalScenePoint(x: .23, y: .2),
|
|
||||||
),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
controller.addUnit(
|
|
||||||
definition: definition,
|
|
||||||
localPlacement: const LocalScenePoint(x: .65, y: .7),
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'catalog additions receive a scattered local presentation placement',
|
|
||||||
() {
|
|
||||||
final container = ProviderContainer();
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
|
||||||
final definition = supportedVehicleDefinitions.first;
|
|
||||||
expect(controller.addUnitAutomatically(definition: definition), isTrue);
|
|
||||||
expect(controller.addUnitAutomatically(definition: definition), isTrue);
|
|
||||||
|
|
||||||
final units = container.read(vehicleWorkspaceProvider).units;
|
|
||||||
expect(units, hasLength(2));
|
|
||||||
expect(units.first.localPlacement, isNot(units.last.localPlacement));
|
|
||||||
for (final unit in units) {
|
|
||||||
expect(unit.localPlacement.x, inInclusiveRange(0.0, 1.0));
|
|
||||||
expect(unit.localPlacement.y, inInclusiveRange(0.0, 1.0));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'local unit names are bounded workspace data, not vehicle configuration',
|
|
||||||
() {
|
|
||||||
final container = ProviderContainer();
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
final controller = container.read(vehicleWorkspaceProvider.notifier);
|
|
||||||
expect(
|
|
||||||
controller.addUnitAutomatically(
|
|
||||||
definition: supportedVehicleDefinitions.first,
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
controller.renameUnit('local-unit-1', ' Ridge observer '),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
container.read(vehicleWorkspaceProvider).units.single.displayName,
|
|
||||||
'Ridge observer',
|
|
||||||
);
|
|
||||||
expect(controller.renameUnit('local-unit-1', ' '), isFalse);
|
|
||||||
expect(
|
|
||||||
container.read(vehicleWorkspaceProvider).units.single.displayName,
|
|
||||||
'Ridge observer',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,683 +0,0 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ground_suite/app/app.dart';
|
|
||||||
import 'package:ground_suite/app/app_shell.dart';
|
|
||||||
import 'package:ground_suite/app/theme.dart';
|
|
||||||
import 'package:ground_suite/features/command/presentation/tactical_command_page.dart';
|
|
||||||
import 'package:ground_suite/features/command/presentation/tactical_controller.dart';
|
|
||||||
import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart';
|
|
||||||
import 'package:ground_suite/features/vehicle_workspace/presentation/vehicle_workspace_controller.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('desktop command dashboard opens the grouped tactical controls', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final app = tester.widget<MaterialApp>(find.byType(MaterialApp));
|
|
||||||
expect(app.themeMode, ThemeMode.dark);
|
|
||||||
expect(find.text('TACTICAL COMMAND'), findsOneWidget);
|
|
||||||
expect(find.text('SIMULATED CAMERA CONCEPT'), findsOneWidget);
|
|
||||||
expect(find.text('DEMO-1021'), findsAtLeastNWidgets(1));
|
|
||||||
expect(find.byKey(const Key('units-menu')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('cursor-menu')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('view-menu')), findsOneWidget);
|
|
||||||
expect(find.text('UNITS'), findsNothing);
|
|
||||||
expect(find.text('MODE: SELECTION'), findsNothing);
|
|
||||||
expect(find.text('VIEW'), findsNothing);
|
|
||||||
expect(
|
|
||||||
tester.getSize(find.byKey(const Key('units-menu'))),
|
|
||||||
const Size(48, 48),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
tester.getSize(find.byKey(const Key('cursor-menu'))),
|
|
||||||
const Size(48, 48),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
tester.getSize(find.byKey(const Key('view-menu'))),
|
|
||||||
const Size(48, 48),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('units-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
expect(find.text('Add local unit'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.text('Requires verified source-to-unit binding'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(find.text('Add local unit'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('UNIT HANGAR'), findsOneWidget);
|
|
||||||
expect(find.text('LOCAL SCENE'), findsAtLeastNWidgets(1));
|
|
||||||
expect(find.byKey(const Key('unit-hangar-strip')), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.quadcopter.local')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.fixed-wing.local')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.tiltrotor-vtol.local')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.helicopter.local')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.rover.local')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.surface-vessel.local')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.quadcopter.local')),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PREPARE UNIT'), findsOneWidget);
|
|
||||||
expect(find.text('QUADCOPTER-01'), findsNothing);
|
|
||||||
expect(
|
|
||||||
ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider).unitDraft,
|
|
||||||
isNotNull,
|
|
||||||
);
|
|
||||||
|
|
||||||
final rectangle = await tester.startGesture(const Offset(620, 500));
|
|
||||||
await rectangle.moveTo(const Offset(760, 560));
|
|
||||||
await rectangle.up();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
final draftState = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider);
|
|
||||||
expect(draftState.panel, TacticalPanel.unitHangar);
|
|
||||||
expect(draftState.unitDraft?.definition.name, 'Quadcopter');
|
|
||||||
|
|
||||||
await tester.tap(find.text('PREPARE UNIT'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('UNIT SETUP'), findsOneWidget);
|
|
||||||
expect(find.text('VEHICLE CONFIGURATION UNAVAILABLE'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.text(
|
|
||||||
'Unavailable until verified binding, compatible schema, command authority, and safe activation are available.',
|
|
||||||
),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(find.text('ADD TO LOCAL SCENE'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(
|
|
||||||
ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider).mode,
|
|
||||||
isA<TacticalBrowseMode>(),
|
|
||||||
);
|
|
||||||
expect(find.text('QUADCOPTER-01'), findsAtLeastNWidgets(1));
|
|
||||||
|
|
||||||
await tester.tap(find.text('SET UP'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('LOCAL UNIT SETUP'), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('local-unit-name-field')), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('dark tactical theme is fixed and uses the palette tokens', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final context = tester.element(find.byType(AppShell));
|
|
||||||
expect(
|
|
||||||
tester.widget<MaterialApp>(find.byType(MaterialApp)).themeMode,
|
|
||||||
ThemeMode.dark,
|
|
||||||
);
|
|
||||||
expect(Theme.of(context).brightness, Brightness.dark);
|
|
||||||
expect(Theme.of(context).cardColor, panelBackground);
|
|
||||||
expect(
|
|
||||||
tester
|
|
||||||
.widget<Text>(
|
|
||||||
find.text(
|
|
||||||
'LOCAL SCENE DATA · NO VEHICLE LINK · COMMAND AUTHORITY UNAVAILABLE',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.style
|
|
||||||
?.color,
|
|
||||||
accentOrange,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('selection console stays local and exposes no vehicle action', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('unit-card-DEMO-1023')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(
|
|
||||||
find.text('LOCAL VISUAL PREVIEW · NO VEHICLE ACTION'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(find.text('DEPLOY'), findsNothing);
|
|
||||||
expect(find.text('CONFIGURE'), findsNothing);
|
|
||||||
|
|
||||||
await tester.tap(find.text('EVENT STREAM'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('Event timeline'), findsOneWidget);
|
|
||||||
await tester.tap(find.text('Tactical workspace'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('TACTICAL COMMAND'), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('pencil cursor draws directly on the map with its toolbar', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider.notifier).beginPencil();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('pencil-hue')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('pencil-saturation')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('pencil-brightness')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('pencil-thickness')), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('pencil-swatch-282')));
|
|
||||||
await tester.drag(
|
|
||||||
find.byKey(const Key('pencil-thickness')),
|
|
||||||
const Offset(70, 0),
|
|
||||||
);
|
|
||||||
await tester.pump();
|
|
||||||
final pencil =
|
|
||||||
ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider).mode
|
|
||||||
as TacticalPencilMode;
|
|
||||||
expect(pencil.hue, 282);
|
|
||||||
expect(pencil.thickness, greaterThan(3));
|
|
||||||
|
|
||||||
final stroke = await tester.startGesture(const Offset(620, 500));
|
|
||||||
await stroke.moveTo(const Offset(680, 540));
|
|
||||||
await tester.pump(const Duration(milliseconds: 20));
|
|
||||||
await stroke.moveTo(const Offset(740, 490));
|
|
||||||
await tester.pump(const Duration(milliseconds: 20));
|
|
||||||
await stroke.up();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsOneWidget);
|
|
||||||
final container = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
expect(container.read(tacticalControllerProvider).drawings, hasLength(1));
|
|
||||||
expect(find.text('DRAWING'), findsNothing);
|
|
||||||
expect(
|
|
||||||
tester.getSize(find.byKey(const Key('tactical-map-overlay-painter'))),
|
|
||||||
isNot(Size.zero),
|
|
||||||
);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('marker cursor names a clicked local marker separately', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('cursor-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Marker cursor'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('MARKER CURSOR'), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('marker-hue')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('marker-saturation')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('marker-brightness')), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('marker-swatch-208')));
|
|
||||||
await tester.tapAt(const Offset(700, 520));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byKey(const Key('marker-name-field')), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.enterText(
|
|
||||||
find.byKey(const Key('marker-name-field')),
|
|
||||||
'North staging note',
|
|
||||||
);
|
|
||||||
await tester.tap(find.text('SAVE MARKER'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final state = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider);
|
|
||||||
expect(state.markers, hasLength(1));
|
|
||||||
expect(state.markers.single.name, 'North staging note');
|
|
||||||
expect(state.markers.single.hue, 208);
|
|
||||||
expect(state.drawings, isEmpty);
|
|
||||||
expect(find.text('North staging note'), findsAtLeastNWidgets(1));
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('a Pencil click alone never creates a marker or drawing', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final container = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
container.read(tacticalControllerProvider.notifier).beginPencil();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tapAt(const Offset(700, 520));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final state = container.read(tacticalControllerProvider);
|
|
||||||
expect(state.drawings, isEmpty);
|
|
||||||
expect(state.markers, isEmpty);
|
|
||||||
expect(find.byKey(const Key('marker-name-field')), findsNothing);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('map marker selection synchronizes the local selection console', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('map-unit-DEMO-1023')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('DEMO-1023'), findsAtLeastNWidgets(1));
|
|
||||||
expect(
|
|
||||||
ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider).selectedUnitIds,
|
|
||||||
{'DEMO-1023'},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('dragging the tactical canvas creates a local group selection', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final gesture = await tester.startGesture(const Offset(660, 520));
|
|
||||||
await gesture.moveTo(const Offset(960, 745));
|
|
||||||
await tester.pump();
|
|
||||||
await gesture.up();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final selected = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider).selectedUnitIds;
|
|
||||||
expect(selected, containsAll(['DEMO-1021', 'DEMO-1023']));
|
|
||||||
expect(find.text('LOCAL GROUP SELECTION'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'map deselection and hidden demo overlays never select invisibly',
|
|
||||||
(tester) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('map-unit-DEMO-1023')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tapAt(const Offset(600, 500));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final container = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
container.read(tacticalControllerProvider).selectedUnitIds,
|
|
||||||
isEmpty,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('view-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Hide Drones'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final gesture = await tester.startGesture(const Offset(660, 520));
|
|
||||||
await gesture.moveTo(const Offset(960, 745));
|
|
||||||
await gesture.up();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(
|
|
||||||
container.read(tacticalControllerProvider).selectedUnitIds,
|
|
||||||
isEmpty,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets('selection cursor ends pencil mode and restores selection', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('cursor-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Pencil cursor'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.tap(find.text('SELECTION CURSOR'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsNothing);
|
|
||||||
expect(FocusManager.instance.primaryFocus, isNotNull);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('hangar selection prepares a local unit before confirmation', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('units-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Add local unit'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.quadcopter.local')),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
final scope = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
expect(scope.read(tacticalControllerProvider).unitDraft, isNotNull);
|
|
||||||
expect(scope.read(vehicleWorkspaceProvider).units, isEmpty);
|
|
||||||
|
|
||||||
await tester.tap(find.text('PREPARE UNIT'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
final name = find.byKey(const Key('local-unit-name-field'));
|
|
||||||
await tester.tap(name);
|
|
||||||
await tester.enterText(name, 'North survey');
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.text('ADD TO LOCAL SCENE'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('North survey'), findsAtLeastNWidgets(1));
|
|
||||||
expect(scope.read(vehicleWorkspaceProvider).units, hasLength(1));
|
|
||||||
expect(
|
|
||||||
scope.read(vehicleWorkspaceProvider).units.single.displayName,
|
|
||||||
'North survey',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider).mode,
|
|
||||||
isA<TacticalBrowseMode>(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('hangar keeps a draft on back and blocks an invalid local name', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('units-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Add local unit'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(
|
|
||||||
find.byKey(const Key('hangar-card-kaptan.vehicle.fixed-wing.local')),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.text('PREPARE UNIT'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final name = find.byKey(const Key('local-unit-name-field'));
|
|
||||||
await tester.tap(name);
|
|
||||||
await tester.enterText(name, ' ');
|
|
||||||
await tester.tap(find.text('ADD TO LOCAL SCENE'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(
|
|
||||||
find.text('Enter a local unit name of up to 32 characters.'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
final scope = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
expect(scope.read(vehicleWorkspaceProvider).units, isEmpty);
|
|
||||||
|
|
||||||
await tester.tap(find.text('BACK TO HANGAR'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('FIXED-WING SELECTED'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).unitDraft?.definition.name,
|
|
||||||
'Fixed-wing',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('hangar supports arrow selection and Escape focus return', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('units-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Add local unit'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final scope = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
scope.read(tacticalControllerProvider).unitDraft?.definition.name,
|
|
||||||
'Fixed-wing',
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('UNIT HANGAR'), findsNothing);
|
|
||||||
final trigger = find.descendant(
|
|
||||||
of: find.byKey(const Key('units-menu')),
|
|
||||||
matching: find.byType(TextButton),
|
|
||||||
);
|
|
||||||
expect(tester.widget<TextButton>(trigger).focusNode?.hasFocus, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('hangar and setup remain usable at 200 percent text scale', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1440, 960));
|
|
||||||
tester.binding.platformDispatcher.textScaleFactorTestValue = 2;
|
|
||||||
addTearDown(() {
|
|
||||||
tester.binding.setSurfaceSize(null);
|
|
||||||
tester.binding.platformDispatcher.clearTextScaleFactorTestValue();
|
|
||||||
});
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final scope = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
final controller = scope.read(tacticalControllerProvider.notifier);
|
|
||||||
controller.showUnitHangar();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byKey(const Key('unit-hangar-strip')), findsOneWidget);
|
|
||||||
controller.chooseUnitDefinition(
|
|
||||||
supportedVehicleDefinitions.first,
|
|
||||||
displayName: 'QUADCOPTER-01',
|
|
||||||
);
|
|
||||||
controller.beginUnitSetup();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('UNIT SETUP'), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('local-unit-name-field')), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('hangar stays available in the short desktop arrangement', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 760));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final scope = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
);
|
|
||||||
scope.read(tacticalControllerProvider.notifier).showUnitHangar();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('UNIT HANGAR'), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('unit-hangar-strip')), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('secondary click cancels the pencil cursor', (tester) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('cursor-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Pencil cursor'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsOneWidget);
|
|
||||||
|
|
||||||
final pointer = TestPointer(42, PointerDeviceKind.mouse);
|
|
||||||
await tester.sendEventToBinding(
|
|
||||||
pointer.down(const Offset(700, 520), buttons: kSecondaryButton),
|
|
||||||
);
|
|
||||||
await tester.sendEventToBinding(pointer.up());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsNothing);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('keyboard pencil keeps a visible draft before finishing', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('cursor-menu')));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.tap(find.text('Pencil cursor'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
for (var index = 0; index < 3; index++) {
|
|
||||||
await tester.tap(find.text('ADD LOCAL POINT'));
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
||||||
}
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('LOCAL POINTS: 3'), findsOneWidget);
|
|
||||||
expect(find.text('FINISH DRAWING'), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.tap(find.text('FINISH DRAWING'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('compact command dashboard remains usable without overflow', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(480, 800));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.text('SIMULATED CAMERA CONCEPT'), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('command dashboard handles 200 percent text scale', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1440, 960));
|
|
||||||
tester.binding.platformDispatcher.textScaleFactorTestValue = 2;
|
|
||||||
addTearDown(() {
|
|
||||||
tester.binding.setSurfaceSize(null);
|
|
||||||
tester.binding.platformDispatcher.clearTextScaleFactorTestValue();
|
|
||||||
});
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find.text(
|
|
||||||
'LOCAL SCENE DATA · NO VEHICLE LINK · COMMAND AUTHORITY UNAVAILABLE',
|
|
||||||
),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('pencil toolbar reflows at 200 percent text scale', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1440, 960));
|
|
||||||
tester.binding.platformDispatcher.textScaleFactorTestValue = 2;
|
|
||||||
addTearDown(() {
|
|
||||||
tester.binding.setSurfaceSize(null);
|
|
||||||
tester.binding.platformDispatcher.clearTextScaleFactorTestValue();
|
|
||||||
});
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(TacticalCommandPage)),
|
|
||||||
).read(tacticalControllerProvider.notifier).beginPencil();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('PENCIL CURSOR'), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('pencil-thickness')), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('short desktop window uses the non-overlapping command layout', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1366, 768));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const GroundSuiteApp());
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.text('SIMULATED CAMERA CONCEPT'), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
}
|
|
||||||