fix: remote dialog issues and optimize command protocol
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user