Intelligent modes: Point of Interest (orbit on the map, live radius and speed), Follow Me (follows the phone), QuickShots on a boxed subject, Course Lock / Home Lock, Tripod and Cinematic, a Free hyperlapse, and Waypoints 2.0 for V2-only aircraft. ActiveTrack gains Spotlight Pro and Circle. Every mode has a first-use tutorial and stops from the rail. Waypoints can resume an interrupted mission, change cruise speed in flight and check terrain clearance (Open-Meteo, on tap). Settings: units, C1/C2 actions, dual-operator master/slave, HDMI/SDI output, a link spectrum chart, Wi-Fi name and password, aircraft name, video cache, battery warning history and pairing, non-smart cell voltages. Calibration: compass interference meter, per-IMU calibration, lens infinity focus and a first-manual-focus prompt. Also an RTK chip, gimbal movement presets and motor tuning, Precision Takeoff gated on the flight controller, and the precise GEO database's server version. Built and unit/widget-tested only; nothing has flown yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
10 KiB
Dart
249 lines
10 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'rc_buttons.dart';
|
|
import 'units.dart';
|
|
import 'virtual_sticks.dart' show StickMode;
|
|
|
|
export 'rc_buttons.dart' show RcButton, RcButtonAction;
|
|
export 'units.dart' show UnitSystem;
|
|
|
|
/// The video overlay grid (camera menu → Other, and the rail's GRID button).
|
|
enum GridStyle {
|
|
off('Off'),
|
|
thirds('Grid'),
|
|
diagonals('Grid + diagonals');
|
|
|
|
const GridStyle(this.label);
|
|
final String label;
|
|
}
|
|
|
|
/// What a long press on the live view does (GO 4's "Long Press" setting); a
|
|
/// single tap always focuses, or meters on fixed-focus cameras.
|
|
enum LongPressAction {
|
|
meter('Spot meter'),
|
|
focus('Focus'),
|
|
off('Nothing');
|
|
|
|
const LongPressAction(this.label);
|
|
final String label;
|
|
}
|
|
|
|
/// Device-local switches for the flight screen, persisted across launches.
|
|
abstract final class FlyPrefs {
|
|
static const String _classicKey = 'fly.classicHud';
|
|
static const String _perfKey = 'fly.perfOverlay';
|
|
static const String _gridKey = 'fly.grid';
|
|
static const String _centerKey = 'fly.centerPoint';
|
|
static const String _longPressKey = 'fly.longPress';
|
|
static const String _routeKey = 'fly.showRoute';
|
|
static const String _termsKey = 'fly.modeTerms';
|
|
static const String _radarKey = 'fly.radar';
|
|
static const String _stickModeKey = 'fly.stickMode';
|
|
static const String _sticksRealKey = 'fly.sticksRealFlight';
|
|
static const String _autoUploadKey = 'fly.autoUploadFlights';
|
|
static const String _liveUrlKey = 'fly.liveStreamUrl';
|
|
static const String _liveAudioKey = 'fly.liveStreamAudio';
|
|
static const String _unitsKey = 'fly.units';
|
|
static const String _c1Key = 'fly.rcC1';
|
|
static const String _c2Key = 'fly.rcC2';
|
|
static const String _videoCacheKey = 'fly.videoCache';
|
|
static const String _videoCacheLimitKey = 'fly.videoCacheLimitMb';
|
|
static const String _lensCalibratedKey = 'fly.lensCalibrated';
|
|
static const String _tutorialsKey = 'fly.modeTutorials';
|
|
|
|
/// Open the pre-P2 `FlightControlPage` instead of the HUD v2 — kept
|
|
/// reachable until P3's safety flows pass their simulator run (plan P2/P3).
|
|
static final ValueNotifier<bool> classicHud = ValueNotifier<bool>(false);
|
|
|
|
/// Flutter's performance overlay on the flight screen, for the P2 "no jank at
|
|
/// 10 Hz" check.
|
|
static final ValueNotifier<bool> perfOverlay = ValueNotifier<bool>(false);
|
|
|
|
static final ValueNotifier<GridStyle> grid = ValueNotifier<GridStyle>(GridStyle.off);
|
|
static final ValueNotifier<bool> centerPoint = ValueNotifier<bool>(true);
|
|
static final ValueNotifier<LongPressAction> longPress = ValueNotifier<LongPressAction>(LongPressAction.meter);
|
|
|
|
/// Draw the flown trail on the map (settings → General).
|
|
static final ValueNotifier<bool> showRoute = ValueNotifier<bool>(true);
|
|
|
|
/// Intelligent flight modes whose first-use terms the pilot accepted (plan P7), by name.
|
|
static final ValueNotifier<Set<String>> acceptedModeTerms = ValueNotifier<Set<String>>(<String>{});
|
|
|
|
/// Draw the vision radar on the video (GO 4's "Display Radar Chart", plan P8).
|
|
static final ValueNotifier<bool> showRadar = ValueNotifier<bool>(true);
|
|
|
|
/// What each on-screen stick does (plan P8); matches the remote's stick mode by default.
|
|
static final ValueNotifier<StickMode> stickMode = ValueNotifier<StickMode>(StickMode.mode2);
|
|
|
|
/// Developer switch: allow on-screen sticks outside the simulator on aircraft
|
|
/// that have a remote. Off by default (plan P8: simulator first).
|
|
static final ValueNotifier<bool> sticksInRealFlight = ValueNotifier<bool>(false);
|
|
|
|
/// Upload each recorded flight to the API Server's logbook when it lands (plan P9).
|
|
static final ValueNotifier<bool> autoUploadFlights = ValueNotifier<bool>(true);
|
|
|
|
/// The RTMP address live streaming sends to (plan P9), `rtmp://host/app/key`.
|
|
static final ValueNotifier<String> liveStreamUrl = ValueNotifier<String>('');
|
|
|
|
/// Send the phone's microphone with the live stream.
|
|
static final ValueNotifier<bool> liveStreamAudio = ValueNotifier<bool>(false);
|
|
|
|
/// Metric or imperial readouts on the HUD and in settings (plan P6).
|
|
static final ValueNotifier<UnitSystem> units = ValueNotifier<UnitSystem>(UnitSystem.metric);
|
|
|
|
/// What the remote's C1 / C2 buttons do (plan P6); GO 4's defaults.
|
|
static final ValueNotifier<RcButtonAction> c1 = ValueNotifier<RcButtonAction>(RcButtonAction.gimbalRecenter);
|
|
static final ValueNotifier<RcButtonAction> c2 = ValueNotifier<RcButtonAction>(RcButtonAction.gimbalToggleDown);
|
|
|
|
/// Record the live feed to the phone while flying (GO 4's video cache, plan P6).
|
|
static final ValueNotifier<bool> videoCache = ValueNotifier<bool>(false);
|
|
|
|
/// The cache's size cap; the oldest recordings go first.
|
|
static final ValueNotifier<int> videoCacheLimitMb = ValueNotifier<int>(2048);
|
|
|
|
/// Cameras (by display name) whose manual-focus lens was calibrated here, so
|
|
/// the first-MF prompt is not repeated (plan P6b).
|
|
static final ValueNotifier<Set<String>> lensCalibrated = ValueNotifier<Set<String>>(<String>{});
|
|
|
|
/// Intelligent modes whose tutorial the pilot has seen (plan P7), by name.
|
|
static final ValueNotifier<Set<String>> seenTutorials = ValueNotifier<Set<String>>(<String>{});
|
|
|
|
static SharedPreferences? _prefs;
|
|
|
|
static Future<void> load() async {
|
|
try {
|
|
_prefs = await SharedPreferences.getInstance();
|
|
classicHud.value = _prefs!.getBool(_classicKey) ?? false;
|
|
perfOverlay.value = _prefs!.getBool(_perfKey) ?? false;
|
|
grid.value = _byName(GridStyle.values, _prefs!.getString(_gridKey)) ?? GridStyle.off;
|
|
centerPoint.value = _prefs!.getBool(_centerKey) ?? true;
|
|
longPress.value = _byName(LongPressAction.values, _prefs!.getString(_longPressKey)) ?? LongPressAction.meter;
|
|
showRoute.value = _prefs!.getBool(_routeKey) ?? true;
|
|
acceptedModeTerms.value = (_prefs!.getStringList(_termsKey) ?? const <String>[]).toSet();
|
|
showRadar.value = _prefs!.getBool(_radarKey) ?? true;
|
|
stickMode.value = _byName(StickMode.values, _prefs!.getString(_stickModeKey)) ?? StickMode.mode2;
|
|
sticksInRealFlight.value = _prefs!.getBool(_sticksRealKey) ?? false;
|
|
autoUploadFlights.value = _prefs!.getBool(_autoUploadKey) ?? true;
|
|
liveStreamUrl.value = _prefs!.getString(_liveUrlKey) ?? '';
|
|
liveStreamAudio.value = _prefs!.getBool(_liveAudioKey) ?? false;
|
|
units.value = _byName(UnitSystem.values, _prefs!.getString(_unitsKey)) ?? UnitSystem.metric;
|
|
c1.value = _byName(RcButtonAction.values, _prefs!.getString(_c1Key)) ?? RcButtonAction.gimbalRecenter;
|
|
c2.value = _byName(RcButtonAction.values, _prefs!.getString(_c2Key)) ?? RcButtonAction.gimbalToggleDown;
|
|
videoCache.value = _prefs!.getBool(_videoCacheKey) ?? false;
|
|
videoCacheLimitMb.value = _prefs!.getInt(_videoCacheLimitKey) ?? 2048;
|
|
lensCalibrated.value = (_prefs!.getStringList(_lensCalibratedKey) ?? const <String>[]).toSet();
|
|
seenTutorials.value = (_prefs!.getStringList(_tutorialsKey) ?? const <String>[]).toSet();
|
|
} catch (_) {
|
|
// Storage unavailable: run on the defaults.
|
|
}
|
|
}
|
|
|
|
static Future<void> setClassicHud(bool on) async {
|
|
classicHud.value = on;
|
|
await _prefs?.setBool(_classicKey, on);
|
|
}
|
|
|
|
static Future<void> setPerfOverlay(bool on) async {
|
|
perfOverlay.value = on;
|
|
await _prefs?.setBool(_perfKey, on);
|
|
}
|
|
|
|
static Future<void> setGrid(GridStyle g) async {
|
|
grid.value = g;
|
|
await _prefs?.setString(_gridKey, g.name);
|
|
}
|
|
|
|
static Future<void> setCenterPoint(bool on) async {
|
|
centerPoint.value = on;
|
|
await _prefs?.setBool(_centerKey, on);
|
|
}
|
|
|
|
static Future<void> setLongPress(LongPressAction a) async {
|
|
longPress.value = a;
|
|
await _prefs?.setString(_longPressKey, a.name);
|
|
}
|
|
|
|
static Future<void> setShowRoute(bool on) async {
|
|
showRoute.value = on;
|
|
await _prefs?.setBool(_routeKey, on);
|
|
}
|
|
|
|
static Future<void> acceptModeTerms(String mode) async {
|
|
if (acceptedModeTerms.value.contains(mode)) return;
|
|
acceptedModeTerms.value = <String>{...acceptedModeTerms.value, mode};
|
|
await _prefs?.setStringList(_termsKey, acceptedModeTerms.value.toList()..sort());
|
|
}
|
|
|
|
static Future<void> setShowRadar(bool on) async {
|
|
showRadar.value = on;
|
|
await _prefs?.setBool(_radarKey, on);
|
|
}
|
|
|
|
static Future<void> setStickMode(StickMode m) async {
|
|
stickMode.value = m;
|
|
await _prefs?.setString(_stickModeKey, m.name);
|
|
}
|
|
|
|
static Future<void> setSticksInRealFlight(bool on) async {
|
|
sticksInRealFlight.value = on;
|
|
await _prefs?.setBool(_sticksRealKey, on);
|
|
}
|
|
|
|
static Future<void> setAutoUploadFlights(bool on) async {
|
|
autoUploadFlights.value = on;
|
|
await _prefs?.setBool(_autoUploadKey, on);
|
|
}
|
|
|
|
static Future<void> setLiveStreamUrl(String url) async {
|
|
liveStreamUrl.value = url.trim();
|
|
await _prefs?.setString(_liveUrlKey, url.trim());
|
|
}
|
|
|
|
static Future<void> setLiveStreamAudio(bool on) async {
|
|
liveStreamAudio.value = on;
|
|
await _prefs?.setBool(_liveAudioKey, on);
|
|
}
|
|
|
|
static Future<void> setUnits(UnitSystem u) async {
|
|
units.value = u;
|
|
await _prefs?.setString(_unitsKey, u.name);
|
|
}
|
|
|
|
static Future<void> setRcButton(RcButton b, RcButtonAction a) async {
|
|
(b == RcButton.c1 ? c1 : c2).value = a;
|
|
await _prefs?.setString(b == RcButton.c1 ? _c1Key : _c2Key, a.name);
|
|
}
|
|
|
|
static RcButtonAction rcButton(RcButton b) => (b == RcButton.c1 ? c1 : c2).value;
|
|
|
|
static Future<void> setVideoCache(bool on) async {
|
|
videoCache.value = on;
|
|
await _prefs?.setBool(_videoCacheKey, on);
|
|
}
|
|
|
|
static Future<void> setVideoCacheLimitMb(int mb) async {
|
|
videoCacheLimitMb.value = mb;
|
|
await _prefs?.setInt(_videoCacheLimitKey, mb);
|
|
}
|
|
|
|
static Future<void> markLensCalibrated(String camera) async {
|
|
if (lensCalibrated.value.contains(camera)) return;
|
|
lensCalibrated.value = <String>{...lensCalibrated.value, camera};
|
|
await _prefs?.setStringList(_lensCalibratedKey, lensCalibrated.value.toList()..sort());
|
|
}
|
|
|
|
static Future<void> markTutorialSeen(String mode) async {
|
|
if (seenTutorials.value.contains(mode)) return;
|
|
seenTutorials.value = <String>{...seenTutorials.value, mode};
|
|
await _prefs?.setStringList(_tutorialsKey, seenTutorials.value.toList()..sort());
|
|
}
|
|
|
|
static T? _byName<T extends Enum>(List<T> values, String? name) {
|
|
for (final T v in values) {
|
|
if (v.name == name) return v;
|
|
}
|
|
return null;
|
|
}
|
|
}
|