fix: remote dialog issues and optimize command protocol

This commit is contained in:
edde746
2026-02-10 09:17:55 +01:00
parent 660267b504
commit 5c3fbfdff8
7 changed files with 30 additions and 290 deletions
+11 -44
View File
@@ -1,59 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
import 'remote_command_type.dart';
part 'remote_command.g.dart';
@JsonSerializable()
class RemoteCommand {
@JsonKey(unknownEnumValue: RemoteCommandType.ping)
final RemoteCommandType type;
final String deviceId;
final String deviceName;
final DateTime timestamp;
final Map<String, dynamic>? data;
RemoteCommand({required this.type, required this.deviceId, required this.deviceName, DateTime? timestamp, this.data})
: timestamp = timestamp ?? DateTime.now();
const RemoteCommand({required this.type, this.data});
factory RemoteCommand.fromJson(Map<String, dynamic> json) => _$RemoteCommandFromJson(json);
Map<String, dynamic> toJson() => _$RemoteCommandToJson(this);
RemoteCommand copyWith({
RemoteCommandType? type,
String? deviceId,
String? deviceName,
DateTime? timestamp,
Map<String, dynamic>? data,
}) {
factory RemoteCommand.fromJson(Map<String, dynamic> json) {
final index = json['t'] as int;
return RemoteCommand(
type: type ?? this.type,
deviceId: deviceId ?? this.deviceId,
deviceName: deviceName ?? this.deviceName,
timestamp: timestamp ?? this.timestamp,
data: data ?? this.data,
type: index < RemoteCommandType.values.length ? RemoteCommandType.values[index] : RemoteCommandType.ping,
data: json['d'] as Map<String, dynamic>?,
);
}
@override
String toString() {
return 'RemoteCommand(type: ${type.name}, device: $deviceName, data: $data)';
Map<String, dynamic> toJson() {
return {
't': type.index,
if (data != null) 'd': data,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is RemoteCommand &&
other.type == type &&
other.deviceId == deviceId &&
other.deviceName == deviceName &&
other.timestamp == timestamp;
}
@override
int get hashCode {
return type.hashCode ^ deviceId.hashCode ^ deviceName.hashCode ^ timestamp.hashCode;
}
String toString() => 'RemoteCommand(${type.name}, data: $data)';
}
@@ -1,67 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'remote_command.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
RemoteCommand _$RemoteCommandFromJson(Map<String, dynamic> json) => RemoteCommand(
type: $enumDecode(_$RemoteCommandTypeEnumMap, json['type'], unknownValue: RemoteCommandType.ping),
deviceId: json['deviceId'] as String,
deviceName: json['deviceName'] as String,
timestamp: json['timestamp'] == null ? null : DateTime.parse(json['timestamp'] as String),
data: json['data'] as Map<String, dynamic>?,
);
Map<String, dynamic> _$RemoteCommandToJson(RemoteCommand instance) => <String, dynamic>{
'type': _$RemoteCommandTypeEnumMap[instance.type]!,
'deviceId': instance.deviceId,
'deviceName': instance.deviceName,
'timestamp': instance.timestamp.toIso8601String(),
'data': instance.data,
};
const _$RemoteCommandTypeEnumMap = {
RemoteCommandType.dpadUp: 'dpadUp',
RemoteCommandType.dpadDown: 'dpadDown',
RemoteCommandType.dpadLeft: 'dpadLeft',
RemoteCommandType.dpadRight: 'dpadRight',
RemoteCommandType.select: 'select',
RemoteCommandType.back: 'back',
RemoteCommandType.contextMenu: 'contextMenu',
RemoteCommandType.play: 'play',
RemoteCommandType.pause: 'pause',
RemoteCommandType.playPause: 'playPause',
RemoteCommandType.stop: 'stop',
RemoteCommandType.seekForward: 'seekForward',
RemoteCommandType.seekBackward: 'seekBackward',
RemoteCommandType.nextTrack: 'nextTrack',
RemoteCommandType.previousTrack: 'previousTrack',
RemoteCommandType.skipIntro: 'skipIntro',
RemoteCommandType.skipCredits: 'skipCredits',
RemoteCommandType.volumeUp: 'volumeUp',
RemoteCommandType.volumeDown: 'volumeDown',
RemoteCommandType.volumeMute: 'volumeMute',
RemoteCommandType.volumeSet: 'volumeSet',
RemoteCommandType.tabNext: 'tabNext',
RemoteCommandType.tabPrevious: 'tabPrevious',
RemoteCommandType.tabDiscover: 'tabDiscover',
RemoteCommandType.tabLibraries: 'tabLibraries',
RemoteCommandType.tabSearch: 'tabSearch',
RemoteCommandType.tabDownloads: 'tabDownloads',
RemoteCommandType.tabSettings: 'tabSettings',
RemoteCommandType.home: 'home',
RemoteCommandType.search: 'search',
RemoteCommandType.subtitles: 'subtitles',
RemoteCommandType.audioTracks: 'audioTracks',
RemoteCommandType.qualitySettings: 'qualitySettings',
RemoteCommandType.fullscreen: 'fullscreen',
RemoteCommandType.ping: 'ping',
RemoteCommandType.pong: 'pong',
RemoteCommandType.deviceInfo: 'deviceInfo',
RemoteCommandType.capabilitiesRequest: 'capabilitiesRequest',
RemoteCommandType.capabilitiesResponse: 'capabilitiesResponse',
RemoteCommandType.disconnect: 'disconnect',
RemoteCommandType.ack: 'ack',
};
@@ -47,145 +47,6 @@ enum RemoteCommandType {
ping,
pong,
deviceInfo,
capabilitiesRequest,
capabilitiesResponse,
disconnect,
ack, // Acknowledgment of received command
}
extension RemoteCommandTypeExtension on RemoteCommandType {
String get displayName {
switch (this) {
case RemoteCommandType.dpadUp:
return 'Up';
case RemoteCommandType.dpadDown:
return 'Down';
case RemoteCommandType.dpadLeft:
return 'Left';
case RemoteCommandType.dpadRight:
return 'Right';
case RemoteCommandType.select:
return 'Select';
case RemoteCommandType.back:
return 'Back';
case RemoteCommandType.contextMenu:
return 'Menu';
case RemoteCommandType.play:
return 'Play';
case RemoteCommandType.pause:
return 'Pause';
case RemoteCommandType.playPause:
return 'Play/Pause';
case RemoteCommandType.stop:
return 'Stop';
case RemoteCommandType.seekForward:
return 'Seek Forward';
case RemoteCommandType.seekBackward:
return 'Seek Backward';
case RemoteCommandType.nextTrack:
return 'Next';
case RemoteCommandType.previousTrack:
return 'Previous';
case RemoteCommandType.skipIntro:
return 'Skip Intro';
case RemoteCommandType.skipCredits:
return 'Skip Credits';
case RemoteCommandType.volumeUp:
return 'Volume Up';
case RemoteCommandType.volumeDown:
return 'Volume Down';
case RemoteCommandType.volumeMute:
return 'Mute';
case RemoteCommandType.volumeSet:
return 'Set Volume';
case RemoteCommandType.tabNext:
return 'Next Tab';
case RemoteCommandType.tabPrevious:
return 'Previous Tab';
case RemoteCommandType.tabDiscover:
return 'Discover';
case RemoteCommandType.tabLibraries:
return 'Libraries';
case RemoteCommandType.tabSearch:
return 'Search';
case RemoteCommandType.tabDownloads:
return 'Downloads';
case RemoteCommandType.tabSettings:
return 'Settings';
case RemoteCommandType.home:
return 'Home';
case RemoteCommandType.search:
return 'Search';
case RemoteCommandType.subtitles:
return 'Subtitles';
case RemoteCommandType.audioTracks:
return 'Audio';
case RemoteCommandType.qualitySettings:
return 'Quality';
case RemoteCommandType.fullscreen:
return 'Fullscreen';
case RemoteCommandType.ping:
return 'Ping';
case RemoteCommandType.pong:
return 'Pong';
case RemoteCommandType.deviceInfo:
return 'Device Info';
case RemoteCommandType.capabilitiesRequest:
return 'Capabilities Request';
case RemoteCommandType.capabilitiesResponse:
return 'Capabilities Response';
case RemoteCommandType.disconnect:
return 'Disconnect';
case RemoteCommandType.ack:
return 'Acknowledgment';
}
}
bool get isNavigationCommand {
return [
RemoteCommandType.dpadUp,
RemoteCommandType.dpadDown,
RemoteCommandType.dpadLeft,
RemoteCommandType.dpadRight,
RemoteCommandType.select,
RemoteCommandType.back,
RemoteCommandType.contextMenu,
].contains(this);
}
bool get isPlaybackCommand {
return [
RemoteCommandType.play,
RemoteCommandType.pause,
RemoteCommandType.playPause,
RemoteCommandType.stop,
RemoteCommandType.seekForward,
RemoteCommandType.seekBackward,
RemoteCommandType.nextTrack,
RemoteCommandType.previousTrack,
RemoteCommandType.skipIntro,
RemoteCommandType.skipCredits,
].contains(this);
}
bool get isVolumeCommand {
return [
RemoteCommandType.volumeUp,
RemoteCommandType.volumeDown,
RemoteCommandType.volumeMute,
RemoteCommandType.volumeSet,
].contains(this);
}
bool get isTabCommand {
return [
RemoteCommandType.tabNext,
RemoteCommandType.tabPrevious,
RemoteCommandType.tabDiscover,
RemoteCommandType.tabLibraries,
RemoteCommandType.tabSearch,
RemoteCommandType.tabDownloads,
RemoteCommandType.tabSettings,
].contains(this);
}
ack,
}
+5 -10
View File
@@ -168,12 +168,14 @@ class CompanionRemoteProvider with ChangeNotifier {
Future<void> _handleDeviceInfo(RemoteCommand command) async {
if (command.data != null) {
final id = command.data!['id'] as String? ?? 'unknown';
final name = command.data!['name'] as String? ?? 'Unknown Device';
final platform = command.data!['platform'] as String? ?? 'unknown';
final role = command.data!['role'] as String?;
appLogger.d('CompanionRemote: Device info - name: ${command.deviceName}, platform: $platform, role: $role');
appLogger.d('CompanionRemote: Device info - name: $name, platform: $platform, role: $role');
final device = RemoteDevice(id: command.deviceId, name: command.deviceName, platform: platform);
final device = RemoteDevice(id: id, name: name, platform: platform);
_session = _session?.copyWith(connectedDevice: device);
notifyListeners();
@@ -275,14 +277,7 @@ class CompanionRemoteProvider with ChangeNotifier {
}
appLogger.d('CompanionRemote: Sending command $type');
final command = RemoteCommand(
type: type,
deviceId: _peerService!.myPeerId ?? 'unknown',
deviceName: _deviceName,
data: data,
);
_peerService!.sendCommand(command);
_peerService!.sendCommand(RemoteCommand(type: type, data: data));
}
void _scheduleReconnect() {
@@ -268,13 +268,13 @@ class CompanionRemotePeerService {
appLogger.d('CompanionRemote: Received command: ${command.type}');
if (_shouldSendAck(command)) {
_sendAck(command, hostDeviceName);
_sendAck(command);
}
_commandReceivedController.add(command);
if (command.type == RemoteCommandType.ping) {
_sendPong(hostDeviceName, hostPlatform);
_sendPong();
}
}
} catch (e) {
@@ -375,13 +375,13 @@ class CompanionRemotePeerService {
appLogger.d('CompanionRemote: Received command: ${command.type}');
if (_shouldSendAck(command)) {
_sendAck(command, deviceName);
_sendAck(command);
}
_commandReceivedController.add(command);
if (command.type == RemoteCommandType.ping) {
_sendPong(deviceName, platform);
_sendPong();
}
}
} catch (e) {
@@ -441,7 +441,7 @@ class CompanionRemotePeerService {
_stopPingTimer();
_pingTimer = Timer.periodic(const Duration(seconds: 5), (_) {
if (isConnected) {
sendCommand(RemoteCommand(type: RemoteCommandType.ping, deviceId: _myPeerId ?? 'unknown', deviceName: 'local'));
sendCommand(const RemoteCommand(type: RemoteCommandType.ping));
}
});
}
@@ -458,34 +458,19 @@ class CompanionRemotePeerService {
command.type != RemoteCommandType.deviceInfo;
}
void _sendAck(RemoteCommand command, String deviceName) {
final ackCommand = RemoteCommand(
type: RemoteCommandType.ack,
deviceId: _myPeerId ?? 'unknown',
deviceName: deviceName,
data: {'originalCommand': command.type.toString()},
);
sendCommand(ackCommand);
void _sendAck(RemoteCommand command) {
sendCommand(const RemoteCommand(type: RemoteCommandType.ack));
}
void _sendPong(String deviceName, String platform) {
sendCommand(
RemoteCommand(
type: RemoteCommandType.pong,
deviceId: _myPeerId ?? 'unknown',
deviceName: deviceName,
data: {'platform': platform},
),
);
void _sendPong() {
sendCommand(const RemoteCommand(type: RemoteCommandType.pong));
}
void sendDeviceInfo(String deviceName, String platform) {
sendCommand(
RemoteCommand(
type: RemoteCommandType.deviceInfo,
deviceId: _myPeerId ?? 'unknown',
deviceName: deviceName,
data: {'platform': platform, 'role': _role?.name},
data: {'id': _myPeerId, 'name': deviceName, 'platform': platform, 'role': _role?.name},
),
);
}
@@ -119,8 +119,6 @@ class CompanionRemoteReceiver {
case RemoteCommandType.pong:
case RemoteCommandType.ack:
case RemoteCommandType.deviceInfo:
case RemoteCommandType.capabilitiesRequest:
case RemoteCommandType.capabilitiesResponse:
case RemoteCommandType.disconnect:
break;
@@ -29,7 +29,8 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
@override
void initState() {
super.initState();
_createSession();
// Defer to avoid notifyListeners() during build phase
WidgetsBinding.instance.addPostFrameCallback((_) => _createSession());
}
Future<void> _createSession() async {
@@ -122,7 +123,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,