e3917e6 hid any row whose key failed its seeding read, which also hid
"Link remote controller": an action key has no value to read. Unreadable is
now tracked apart from supported, and only rows that show a value hide on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
3.5 KiB
Dart
127 lines
3.5 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
import '../../dji_service.dart';
|
|
import 'settings_overlay.dart';
|
|
|
|
/// KeyManager values the settings sheet watches (plan P6), keyed by
|
|
/// [DjiKey.id], from the bridge's `key` events — overlaid with the sheet's
|
|
/// optimistic writes through [SettingsOverlay], like camera and gimbal settings.
|
|
///
|
|
/// [supported] records `isKeySupported` per key: a row hides once its key is
|
|
/// known to be unsupported, and stays visible (as "—") while still unknown.
|
|
class KeyState extends ChangeNotifier with SettingsOverlay {
|
|
final Map<String, bool> _supported = <String, bool>{};
|
|
final Set<String> _unreadable = <String>{};
|
|
|
|
/// True / false once asked, null while unknown.
|
|
bool? supported(DjiKey key) => _supported[key.id];
|
|
|
|
Object? value(DjiKey key) => setting(key.id);
|
|
|
|
bool pendingFor(DjiKey key) => isPending(key.id);
|
|
|
|
void setSupported(DjiKey key, bool on) {
|
|
if (_supported[key.id] == on) return;
|
|
_supported[key.id] = on;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// The aircraft calls the key supported but will not read it (the Mavic Pro
|
|
/// remote's gimbal dial speed). Rows that show its value hide; action rows,
|
|
/// which never read it, do not.
|
|
bool unreadable(DjiKey key) => _unreadable.contains(key.id);
|
|
|
|
void markUnreadable(String id) {
|
|
if (reportedSettings.containsKey(id) || !_unreadable.add(id)) return;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// One `key` event: `{id, value}`.
|
|
void apply(String id, Object? value) {
|
|
_unreadable.remove(id);
|
|
replaceReported(<String, dynamic>{...reportedSettings, id: value});
|
|
notifyListeners();
|
|
}
|
|
|
|
/// A new aircraft answers `isKeySupported` afresh.
|
|
void forgetSupport() {
|
|
if (_supported.isEmpty && _unreadable.isEmpty) return;
|
|
_supported.clear();
|
|
_unreadable.clear();
|
|
notifyListeners();
|
|
}
|
|
|
|
void clear() {
|
|
_supported.clear();
|
|
_unreadable.clear();
|
|
clearSettings();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// The `listenKey` subscriptions one screen holds open — the settings sheet
|
|
/// while it is up. [watch] asks `isKeySupported` once per key and listens to
|
|
/// the supported ones; [dispose] cancels them all.
|
|
class KeySubscriptions {
|
|
KeySubscriptions(this.dji, this.state);
|
|
|
|
final DjiService dji;
|
|
final KeyState state;
|
|
|
|
final Set<DjiKey> _asked = <DjiKey>{};
|
|
final Set<String> _listening = <String>{};
|
|
bool _disposed = false;
|
|
|
|
Future<void> watch(Iterable<DjiKey> keys) async {
|
|
await Future.wait(<Future<void>>[
|
|
for (final DjiKey k in keys)
|
|
if (_asked.add(k)) _watchOne(k),
|
|
]);
|
|
}
|
|
|
|
Future<void> _watchOne(DjiKey key) async {
|
|
bool ok;
|
|
try {
|
|
ok = await dji.isKeySupported(key);
|
|
} catch (_) {
|
|
ok = false;
|
|
}
|
|
if (_disposed) return;
|
|
state.setSupported(key, ok);
|
|
if (!ok) return;
|
|
try {
|
|
final String id = await dji.listenKey(key);
|
|
if (_disposed) {
|
|
await dji.cancelKey(id);
|
|
} else {
|
|
_listening.add(id);
|
|
}
|
|
} catch (_) {
|
|
// The row shows "—" until a value arrives some other way.
|
|
}
|
|
}
|
|
|
|
/// Asks every key again — after a reconnect or product change.
|
|
Future<void> rewatch(Iterable<DjiKey> keys) async {
|
|
await _cancelAll();
|
|
_asked.clear();
|
|
state.forgetSupport();
|
|
await watch(keys);
|
|
}
|
|
|
|
Future<void> _cancelAll() async {
|
|
final List<String> ids = _listening.toList();
|
|
_listening.clear();
|
|
for (final String id in ids) {
|
|
try {
|
|
await dji.cancelKey(id);
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
void dispose() {
|
|
_disposed = true;
|
|
_cancelAll();
|
|
}
|
|
}
|