Migrate companion remote to WebSocket

This commit is contained in:
Matt Vogel
2026-02-09 18:45:10 -05:00
parent 199d634cb2
commit e30c0b4370
24 changed files with 1035 additions and 1788 deletions
File diff suppressed because it is too large Load Diff
@@ -10,6 +10,7 @@ class RecentRemoteSession {
final String deviceName; final String deviceName;
final String platform; final String platform;
final DateTime lastConnected; final DateTime lastConnected;
final String? hostAddress; // Format: "ip:port"
RecentRemoteSession({ RecentRemoteSession({
required this.sessionId, required this.sessionId,
@@ -17,25 +18,32 @@ class RecentRemoteSession {
required this.deviceName, required this.deviceName,
required this.platform, required this.platform,
required this.lastConnected, required this.lastConnected,
this.hostAddress,
}); });
factory RecentRemoteSession.fromJson(Map<String, dynamic> json) => _$RecentRemoteSessionFromJson(json); factory RecentRemoteSession.fromJson(Map<String, dynamic> json) => _$RecentRemoteSessionFromJson(json);
Map<String, dynamic> toJson() => _$RecentRemoteSessionToJson(this); Map<String, dynamic> toJson() => _$RecentRemoteSessionToJson(this);
/// Create from QR code data (format: "sessionId:pin:deviceName:platform") /// Create from QR code data (format: "ip|port|sessionId|pin")
factory RecentRemoteSession.fromQrData(String qrData) { factory RecentRemoteSession.fromQrData(String qrData) {
final parts = qrData.split(':'); final parts = qrData.split('|');
if (parts.length < 2) { if (parts.length < 4) {
throw FormatException('Invalid QR code format'); throw FormatException('Invalid QR code format - expected ip|port|sessionId|pin');
} }
final ip = parts[0];
final port = parts[1];
final sessionId = parts[2];
final pin = parts[3];
return RecentRemoteSession( return RecentRemoteSession(
sessionId: parts[0], sessionId: sessionId,
pin: parts[1], pin: pin,
deviceName: parts.length > 2 ? parts[2] : 'Unknown Device', deviceName: 'Unknown Device',
platform: parts.length > 3 ? parts[3] : 'unknown', platform: 'unknown',
lastConnected: DateTime.now(), lastConnected: DateTime.now(),
hostAddress: '$ip:$port',
); );
} }
@@ -12,6 +12,7 @@ RecentRemoteSession _$RecentRemoteSessionFromJson(Map<String, dynamic> json) =>
deviceName: json['deviceName'] as String, deviceName: json['deviceName'] as String,
platform: json['platform'] as String, platform: json['platform'] as String,
lastConnected: DateTime.parse(json['lastConnected'] as String), lastConnected: DateTime.parse(json['lastConnected'] as String),
hostAddress: json['hostAddress'] as String?,
); );
Map<String, dynamic> _$RecentRemoteSessionToJson(RecentRemoteSession instance) => <String, dynamic>{ Map<String, dynamic> _$RecentRemoteSessionToJson(RecentRemoteSession instance) => <String, dynamic>{
@@ -20,4 +21,5 @@ Map<String, dynamic> _$RecentRemoteSessionToJson(RecentRemoteSession instance) =
'deviceName': instance.deviceName, 'deviceName': instance.deviceName,
'platform': instance.platform, 'platform': instance.platform,
'lastConnected': instance.lastConnected.toIso8601String(), 'lastConnected': instance.lastConnected.toIso8601String(),
'hostAddress': instance.hostAddress,
}; };
@@ -90,17 +90,19 @@ class RemoteSession {
RemoteSessionRole? role, RemoteSessionRole? role,
RemoteSessionStatus? status, RemoteSessionStatus? status,
RemoteDevice? connectedDevice, RemoteDevice? connectedDevice,
bool clearConnectedDevice = false,
DateTime? createdAt, DateTime? createdAt,
String? errorMessage, String? errorMessage,
bool clearErrorMessage = false,
}) { }) {
return RemoteSession( return RemoteSession(
sessionId: sessionId ?? this.sessionId, sessionId: sessionId ?? this.sessionId,
pin: pin ?? this.pin, pin: pin ?? this.pin,
role: role ?? this.role, role: role ?? this.role,
status: status ?? this.status, status: status ?? this.status,
connectedDevice: connectedDevice ?? this.connectedDevice, connectedDevice: clearConnectedDevice ? null : (connectedDevice ?? this.connectedDevice),
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: clearErrorMessage ? null : (errorMessage ?? this.errorMessage),
); );
} }
} }
+12 -20
View File
@@ -6,23 +6,15 @@ part of 'play_queue_response.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
PlayQueueResponse _$PlayQueueResponseFromJson(Map<String, dynamic> json) => PlayQueueResponse _$PlayQueueResponseFromJson(Map<String, dynamic> json) => PlayQueueResponse(
PlayQueueResponse( playQueueID: (json['playQueueID'] as num).toInt(),
playQueueID: (json['playQueueID'] as num).toInt(), playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?)?.toInt(),
playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?) playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?)?.toInt(),
?.toInt(), playQueueSelectedMetadataItemID: json['playQueueSelectedMetadataItemID'] as String?,
playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?) playQueueShuffled: const BoolOrIntConverter().fromJson(json['playQueueShuffled'] as Object),
?.toInt(), playQueueSourceURI: json['playQueueSourceURI'] as String?,
playQueueSelectedMetadataItemID: playQueueTotalCount: (json['playQueueTotalCount'] as num?)?.toInt(),
json['playQueueSelectedMetadataItemID'] as String?, playQueueVersion: (json['playQueueVersion'] as num).toInt(),
playQueueShuffled: const BoolOrIntConverter().fromJson( size: (json['size'] as num?)?.toInt(),
json['playQueueShuffled'] as Object, items: (json['Metadata'] as List<dynamic>?)?.map((e) => PlexMetadata.fromJson(e as Map<String, dynamic>)).toList(),
), );
playQueueSourceURI: json['playQueueSourceURI'] as String?,
playQueueTotalCount: (json['playQueueTotalCount'] as num?)?.toInt(),
playQueueVersion: (json['playQueueVersion'] as num).toInt(),
size: (json['size'] as num?)?.toInt(),
items: (json['Metadata'] as List<dynamic>?)
?.map((e) => PlexMetadata.fromJson(e as Map<String, dynamic>))
.toList(),
);
+12 -13
View File
@@ -19,16 +19,15 @@ PlexLibrary _$PlexLibraryFromJson(Map<String, dynamic> json) => PlexLibrary(
hidden: (json['hidden'] as num?)?.toInt(), hidden: (json['hidden'] as num?)?.toInt(),
); );
Map<String, dynamic> _$PlexLibraryToJson(PlexLibrary instance) => Map<String, dynamic> _$PlexLibraryToJson(PlexLibrary instance) => <String, dynamic>{
<String, dynamic>{ 'key': instance.key,
'key': instance.key, 'title': instance.title,
'title': instance.title, 'type': instance.type,
'type': instance.type, 'agent': instance.agent,
'agent': instance.agent, 'scanner': instance.scanner,
'scanner': instance.scanner, 'language': instance.language,
'language': instance.language, 'uuid': instance.uuid,
'uuid': instance.uuid, 'updatedAt': instance.updatedAt,
'updatedAt': instance.updatedAt, 'createdAt': instance.createdAt,
'createdAt': instance.createdAt, 'hidden': instance.hidden,
'hidden': instance.hidden, };
};
+46 -49
View File
@@ -41,9 +41,7 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
leafCount: (json['leafCount'] as num?)?.toInt(), leafCount: (json['leafCount'] as num?)?.toInt(),
viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(), viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(),
childCount: (json['childCount'] as num?)?.toInt(), childCount: (json['childCount'] as num?)?.toInt(),
role: (json['Role'] as List<dynamic>?) role: (json['Role'] as List<dynamic>?)?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>)).toList(),
?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>))
.toList(),
audioLanguage: json['audioLanguage'] as String?, audioLanguage: json['audioLanguage'] as String?,
subtitleLanguage: json['subtitleLanguage'] as String?, subtitleLanguage: json['subtitleLanguage'] as String?,
playlistItemID: (json['playlistItemID'] as num?)?.toInt(), playlistItemID: (json['playlistItemID'] as num?)?.toInt(),
@@ -54,49 +52,48 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
clearLogo: json['clearLogo'] as String?, clearLogo: json['clearLogo'] as String?,
); );
Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => <String, dynamic>{
<String, dynamic>{ 'ratingKey': instance.ratingKey,
'ratingKey': instance.ratingKey, 'key': instance.key,
'key': instance.key, 'guid': instance.guid,
'guid': instance.guid, 'studio': instance.studio,
'studio': instance.studio, 'type': instance.type,
'type': instance.type, 'title': instance.title,
'title': instance.title, 'titleSort': instance.titleSort,
'titleSort': instance.titleSort, 'contentRating': instance.contentRating,
'contentRating': instance.contentRating, 'summary': instance.summary,
'summary': instance.summary, 'rating': instance.rating,
'rating': instance.rating, 'audienceRating': instance.audienceRating,
'audienceRating': instance.audienceRating, 'year': instance.year,
'year': instance.year, 'originallyAvailableAt': instance.originallyAvailableAt,
'originallyAvailableAt': instance.originallyAvailableAt, 'thumb': instance.thumb,
'thumb': instance.thumb, 'art': instance.art,
'art': instance.art, 'duration': instance.duration,
'duration': instance.duration, 'addedAt': instance.addedAt,
'addedAt': instance.addedAt, 'updatedAt': instance.updatedAt,
'updatedAt': instance.updatedAt, 'lastViewedAt': instance.lastViewedAt,
'lastViewedAt': instance.lastViewedAt, 'grandparentTitle': instance.grandparentTitle,
'grandparentTitle': instance.grandparentTitle, 'grandparentThumb': instance.grandparentThumb,
'grandparentThumb': instance.grandparentThumb, 'grandparentArt': instance.grandparentArt,
'grandparentArt': instance.grandparentArt, 'grandparentRatingKey': instance.grandparentRatingKey,
'grandparentRatingKey': instance.grandparentRatingKey, 'parentTitle': instance.parentTitle,
'parentTitle': instance.parentTitle, 'parentThumb': instance.parentThumb,
'parentThumb': instance.parentThumb, 'parentRatingKey': instance.parentRatingKey,
'parentRatingKey': instance.parentRatingKey, 'parentIndex': instance.parentIndex,
'parentIndex': instance.parentIndex, 'index': instance.index,
'index': instance.index, 'grandparentTheme': instance.grandparentTheme,
'grandparentTheme': instance.grandparentTheme, 'viewOffset': instance.viewOffset,
'viewOffset': instance.viewOffset, 'viewCount': instance.viewCount,
'viewCount': instance.viewCount, 'leafCount': instance.leafCount,
'leafCount': instance.leafCount, 'viewedLeafCount': instance.viewedLeafCount,
'viewedLeafCount': instance.viewedLeafCount, 'childCount': instance.childCount,
'childCount': instance.childCount, 'Role': instance.role,
'Role': instance.role, 'audioLanguage': instance.audioLanguage,
'audioLanguage': instance.audioLanguage, 'subtitleLanguage': instance.subtitleLanguage,
'subtitleLanguage': instance.subtitleLanguage, 'playlistItemID': instance.playlistItemID,
'playlistItemID': instance.playlistItemID, 'playQueueItemID': instance.playQueueItemID,
'playQueueItemID': instance.playQueueItemID, 'librarySectionID': instance.librarySectionID,
'librarySectionID': instance.librarySectionID, 'ratingImage': instance.ratingImage,
'ratingImage': instance.ratingImage, 'audienceRatingImage': instance.audienceRatingImage,
'audienceRatingImage': instance.audienceRatingImage, 'clearLogo': instance.clearLogo,
'clearLogo': instance.clearLogo, };
};
+19 -20
View File
@@ -26,23 +26,22 @@ PlexPlaylist _$PlexPlaylistFromJson(Map<String, dynamic> json) => PlexPlaylist(
thumb: json['thumb'] as String?, thumb: json['thumb'] as String?,
); );
Map<String, dynamic> _$PlexPlaylistToJson(PlexPlaylist instance) => Map<String, dynamic> _$PlexPlaylistToJson(PlexPlaylist instance) => <String, dynamic>{
<String, dynamic>{ 'ratingKey': instance.ratingKey,
'ratingKey': instance.ratingKey, 'key': instance.key,
'key': instance.key, 'type': instance.type,
'type': instance.type, 'title': instance.title,
'title': instance.title, 'summary': instance.summary,
'summary': instance.summary, 'smart': instance.smart,
'smart': instance.smart, 'playlistType': instance.playlistType,
'playlistType': instance.playlistType, 'duration': instance.duration,
'duration': instance.duration, 'leafCount': instance.leafCount,
'leafCount': instance.leafCount, 'composite': instance.composite,
'composite': instance.composite, 'addedAt': instance.addedAt,
'addedAt': instance.addedAt, 'updatedAt': instance.updatedAt,
'updatedAt': instance.updatedAt, 'lastViewedAt': instance.lastViewedAt,
'lastViewedAt': instance.lastViewedAt, 'viewCount': instance.viewCount,
'viewCount': instance.viewCount, 'content': instance.content,
'content': instance.content, 'guid': instance.guid,
'guid': instance.guid, 'thumb': instance.thumb,
'thumb': instance.thumb, };
};
+27 -10
View File
@@ -36,6 +36,7 @@ class CompanionRemoteProvider with ChangeNotifier {
bool _intentionalDisconnect = false; bool _intentionalDisconnect = false;
String? _lastSessionId; String? _lastSessionId;
String? _lastPin; String? _lastPin;
String? _lastHostAddress;
int get reconnectAttempts => _reconnectAttempts; int get reconnectAttempts => _reconnectAttempts;
@@ -130,7 +131,10 @@ class CompanionRemoteProvider with ChangeNotifier {
_deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) { _deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) {
appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)'); appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)');
if (_intentionalDisconnect) { if (_intentionalDisconnect) {
_session = _session?.copyWith(status: RemoteSessionStatus.disconnected, connectedDevice: null); _session = _session?.copyWith(
status: RemoteSessionStatus.disconnected,
clearConnectedDevice: true,
);
notifyListeners(); notifyListeners();
} else { } else {
_session = _session?.copyWith(status: RemoteSessionStatus.reconnecting); _session = _session?.copyWith(status: RemoteSessionStatus.reconnecting);
@@ -182,7 +186,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_statusSubscription = null; _statusSubscription = null;
} }
Future<({String sessionId, String pin})> createSession() async { Future<({String sessionId, String pin, String address})> createSession() async {
await leaveSession(); await leaveSession();
appLogger.d('CompanionRemote: Creating session as host'); appLogger.d('CompanionRemote: Creating session as host');
@@ -201,7 +205,9 @@ class CompanionRemoteProvider with ChangeNotifier {
); );
notifyListeners(); notifyListeners();
appLogger.d('CompanionRemote: Session created - ID: ${result.sessionId}, PIN: ${result.pin}'); appLogger.d(
'CompanionRemote: Session created - ID: ${result.sessionId}, PIN: ${result.pin}, Address: ${result.address}',
);
return result; return result;
} catch (e) { } catch (e) {
@@ -218,13 +224,14 @@ class CompanionRemoteProvider with ChangeNotifier {
} }
} }
Future<void> joinSession(String sessionId, String pin) async { Future<void> joinSession(String sessionId, String pin, String hostAddress) async {
await leaveSession(); await leaveSession();
_lastSessionId = sessionId; _lastSessionId = sessionId;
_lastPin = pin; _lastPin = pin;
_lastHostAddress = hostAddress;
appLogger.d('CompanionRemote: Joining session - ID: $sessionId'); appLogger.d('CompanionRemote: Joining session - ID: $sessionId, Host: $hostAddress');
_peerService = CompanionRemotePeerService(); _peerService = CompanionRemotePeerService();
_setupPeerServiceListeners(); _setupPeerServiceListeners();
@@ -238,7 +245,7 @@ class CompanionRemoteProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
try { try {
await _peerService!.joinSession(sessionId, pin, _deviceName, _platform); await _peerService!.joinSession(sessionId, pin, _deviceName, _platform, hostAddress);
_session = _session?.copyWith(status: RemoteSessionStatus.connected); _session = _session?.copyWith(status: RemoteSessionStatus.connected);
notifyListeners(); notifyListeners();
@@ -289,7 +296,7 @@ class CompanionRemoteProvider with ChangeNotifier {
} }
Future<void> _attemptReconnect() async { Future<void> _attemptReconnect() async {
if (_lastSessionId == null || _lastPin == null) { if (_lastSessionId == null || _lastPin == null || _lastHostAddress == null) {
appLogger.w('CompanionRemote: No stored credentials for reconnect'); appLogger.w('CompanionRemote: No stored credentials for reconnect');
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost'); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost');
notifyListeners(); notifyListeners();
@@ -305,7 +312,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_peerService = CompanionRemotePeerService(); _peerService = CompanionRemotePeerService();
_setupPeerServiceListeners(); _setupPeerServiceListeners();
await _peerService!.joinSession(_lastSessionId!, _lastPin!, _deviceName, _platform); await _peerService!.joinSession(_lastSessionId!, _lastPin!, _deviceName, _platform, _lastHostAddress!);
_session = _session?.copyWith(status: RemoteSessionStatus.connected); _session = _session?.copyWith(status: RemoteSessionStatus.connected);
_reconnectAttempts = 0; _reconnectAttempts = 0;
@@ -330,7 +337,10 @@ class CompanionRemoteProvider with ChangeNotifier {
void cancelReconnect() { void cancelReconnect() {
_reconnectTimer?.cancel(); _reconnectTimer?.cancel();
_reconnectAttempts = 0; _reconnectAttempts = 0;
_session = _session?.copyWith(status: RemoteSessionStatus.disconnected, connectedDevice: null); _session = _session?.copyWith(
status: RemoteSessionStatus.disconnected,
clearConnectedDevice: true,
);
notifyListeners(); notifyListeners();
} }
@@ -479,6 +489,7 @@ class CompanionRemoteProvider with ChangeNotifier {
deviceName: deviceToSave.name, deviceName: deviceToSave.name,
platform: deviceToSave.platform, platform: deviceToSave.platform,
lastConnected: DateTime.now(), lastConnected: DateTime.now(),
hostAddress: _peerService?.hostAddress,
); );
if (_discoveryService != null) { if (_discoveryService != null) {
@@ -488,7 +499,13 @@ class CompanionRemoteProvider with ChangeNotifier {
/// Connect to a recent session /// Connect to a recent session
Future<void> connectToRecentSession(RecentRemoteSession session) async { Future<void> connectToRecentSession(RecentRemoteSession session) async {
await joinSession(session.sessionId, session.pin); if (session.hostAddress == null) {
throw const RemotePeerError(
type: RemotePeerErrorType.invalidSession,
message: 'No host address available for this session. Please scan a new QR code.',
);
}
await joinSession(session.sessionId, session.pin, session.hostAddress!);
} }
/// Remove a recent session /// Remove a recent session
@@ -35,14 +35,8 @@ class _MobileRemoteScreenState extends State<MobileRemoteScreen> {
title: const Text('Disconnect'), title: const Text('Disconnect'),
content: const Text('Do you want to disconnect from the remote session?'), content: const Text('Do you want to disconnect from the remote session?'),
actions: [ actions: [
TextButton( TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
onPressed: () => Navigator.pop(context, false), TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Disconnect')),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Disconnect'),
),
], ],
), ),
); );
@@ -68,10 +62,7 @@ class _MobileRemoteScreenState extends State<MobileRemoteScreen> {
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text('Reconnecting...', style: Theme.of(context).textTheme.titleLarge),
'Reconnecting...',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Attempt ${provider.reconnectAttempts} of 5', 'Attempt ${provider.reconnectAttempts} of 5',
@@ -81,15 +72,9 @@ class _MobileRemoteScreenState extends State<MobileRemoteScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
OutlinedButton( OutlinedButton(onPressed: () => provider.cancelReconnect(), child: const Text('Cancel')),
onPressed: () => provider.cancelReconnect(),
child: const Text('Cancel'),
),
const SizedBox(width: 16), const SizedBox(width: 16),
FilledButton( FilledButton(onPressed: () => provider.retryReconnectNow(), child: const Text('Retry Now')),
onPressed: () => provider.retryReconnectNow(),
child: const Text('Retry Now'),
),
], ],
), ),
], ],
@@ -114,10 +99,7 @@ class _MobileRemoteScreenState extends State<MobileRemoteScreen> {
const SizedBox(height: 32), const SizedBox(height: 32),
FilledButton.icon( FilledButton.icon(
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(context, MaterialPageRoute(builder: (context) => const PairingScreen()));
context,
MaterialPageRoute(builder: (context) => const PairingScreen()),
);
}, },
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
label: const Text('Connect to Device'), label: const Text('Connect to Device'),
@@ -141,10 +123,7 @@ class _RemoteControlLayout extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (PlatformDetector.isDesktop(context)) { if (PlatformDetector.isDesktop(context)) {
return Center( return Center(
child: ConstrainedBox( child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 400), child: const _RemoteControlContent()),
constraints: const BoxConstraints(maxWidth: 400),
child: const _RemoteControlContent(),
),
); );
} }
@@ -195,10 +174,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
child: Row( child: Row(
children: [ children: [
Icon( Icon(Icons.computer, color: Theme.of(context).colorScheme.onPrimaryContainer),
Icons.computer,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
@@ -206,15 +182,15 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
children: [ children: [
Text( Text(
device.name, device.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(
color: Theme.of(context).colorScheme.onPrimaryContainer, context,
), ).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),
), ),
Text( Text(
device.platform, device.platform,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(
color: Theme.of(context).colorScheme.onPrimaryContainer, context,
), ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),
), ),
], ],
), ),
@@ -222,10 +198,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
Container( Container(
width: 8, width: 8,
height: 8, height: 8,
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle),
color: Colors.green,
shape: BoxShape.circle,
),
), ),
], ],
), ),
@@ -269,16 +242,8 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_RemoteButton( _RemoteButton(icon: Icons.home, label: 'Home', onPressed: () => _sendCommand(RemoteCommandType.home)),
icon: Icons.home, _RemoteButton(icon: Icons.arrow_back, label: 'Back', onPressed: () => _sendCommand(RemoteCommandType.back)),
label: 'Home',
onPressed: () => _sendCommand(RemoteCommandType.home),
),
_RemoteButton(
icon: Icons.arrow_back,
label: 'Back',
onPressed: () => _sendCommand(RemoteCommandType.back),
),
_RemoteButton( _RemoteButton(
icon: Icons.menu, icon: Icons.menu,
label: 'Menu', label: 'Menu',
@@ -287,14 +252,9 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
], ],
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
Center( Center(child: _DPad(onCommand: _sendCommand)),
child: _DPad(onCommand: _sendCommand),
),
const SizedBox(height: 32), const SizedBox(height: 32),
Text( Text('Tab Navigation', style: Theme.of(context).textTheme.titleMedium),
'Tab Navigation',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Wrap( Wrap(
spacing: 8, spacing: 8,
@@ -370,11 +330,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
onPressed: () => _sendCommand(RemoteCommandType.seekBackward), onPressed: () => _sendCommand(RemoteCommandType.seekBackward),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
_RemoteButton( _RemoteButton(icon: Icons.stop, label: 'Stop', onPressed: () => _sendCommand(RemoteCommandType.stop)),
icon: Icons.stop,
label: 'Stop',
onPressed: () => _sendCommand(RemoteCommandType.stop),
),
const SizedBox(width: 16), const SizedBox(width: 16),
_RemoteButton( _RemoteButton(
icon: Icons.forward_10, icon: Icons.forward_10,
@@ -384,10 +340,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
], ],
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
Text( Text('Volume', style: Theme.of(context).textTheme.titleMedium),
'Volume',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -424,11 +377,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
runSpacing: 12, runSpacing: 12,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: [ children: [
_RemoteCard( _RemoteCard(icon: Icons.search, label: 'Search', onPressed: _showSearchSheet),
icon: Icons.search,
label: 'Search',
onPressed: _showSearchSheet,
),
_RemoteCard( _RemoteCard(
icon: Icons.fullscreen, icon: Icons.fullscreen,
label: 'Fullscreen', label: 'Fullscreen',
@@ -583,19 +532,12 @@ class _RemoteButton extends StatelessWidget {
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();
onPressed(); onPressed();
}, },
style: FilledButton.styleFrom( style: FilledButton.styleFrom(padding: EdgeInsets.zero, shape: const CircleBorder()),
padding: EdgeInsets.zero,
shape: const CircleBorder(),
),
child: Icon(icon, size: iconSize), child: Icon(icon, size: iconSize),
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(label, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center),
label,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
], ],
); );
} }
@@ -606,11 +548,7 @@ class _RemoteChip extends StatelessWidget {
final String label; final String label;
final VoidCallback onPressed; final VoidCallback onPressed;
const _RemoteChip({ const _RemoteChip({required this.icon, required this.label, required this.onPressed});
required this.icon,
required this.label,
required this.onPressed,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -654,12 +592,7 @@ class _SearchBottomSheetState extends State<_SearchBottomSheet> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom, left: 16, right: 16, top: 16),
bottom: MediaQuery.viewInsetsOf(context).bottom,
left: 16,
right: 16,
top: 16,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -669,13 +602,8 @@ class _SearchBottomSheetState extends State<_SearchBottomSheet> {
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search on desktop...', hintText: 'Search on desktop...',
prefixIcon: const Icon(Icons.search), prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton( suffixIcon: IconButton(icon: const Icon(Icons.send), onPressed: () => _submit(_controller.text)),
icon: const Icon(Icons.send), border: OutlineInputBorder(borderRadius: BorderRadius.circular(100)),
onPressed: () => _submit(_controller.text),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
),
), ),
onSubmitted: _submit, onSubmitted: _submit,
), ),
@@ -691,11 +619,7 @@ class _RemoteCard extends StatelessWidget {
final String label; final String label;
final VoidCallback onPressed; final VoidCallback onPressed;
const _RemoteCard({ const _RemoteCard({required this.icon, required this.label, required this.onPressed});
required this.icon,
required this.label,
required this.onPressed,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -714,11 +638,7 @@ class _RemoteCard extends StatelessWidget {
children: [ children: [
Icon(icon, size: 32), Icon(icon, size: 32),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(label, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center),
label,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
], ],
), ),
), ),
@@ -17,6 +17,7 @@ class PairingScreen extends StatefulWidget {
} }
class _PairingScreenState extends State<PairingScreen> { class _PairingScreenState extends State<PairingScreen> {
final _hostAddressController = TextEditingController();
final _sessionIdController = TextEditingController(); final _sessionIdController = TextEditingController();
final _pinController = TextEditingController(); final _pinController = TextEditingController();
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
@@ -44,6 +45,7 @@ class _PairingScreenState extends State<PairingScreen> {
@override @override
void dispose() { void dispose() {
_hostAddressController.dispose();
_sessionIdController.dispose(); _sessionIdController.dispose();
_pinController.dispose(); _pinController.dispose();
_scannerController?.dispose(); _scannerController?.dispose();
@@ -105,7 +107,11 @@ class _PairingScreenState extends State<PairingScreen> {
try { try {
final provider = context.read<CompanionRemoteProvider>(); final provider = context.read<CompanionRemoteProvider>();
await provider.joinSession(_sessionIdController.text.trim().toUpperCase(), _pinController.text.trim()); await provider.joinSession(
_sessionIdController.text.trim().toUpperCase(),
_pinController.text.trim(),
_hostAddressController.text.trim(),
);
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -133,26 +139,33 @@ class _PairingScreenState extends State<PairingScreen> {
if (data == _lastScannedCode) return; if (data == _lastScannedCode) return;
_lastScannedCode = data; _lastScannedCode = data;
final parts = data.split(':'); // New format: ip|port|sessionId|pin (4 parts separated by pipe)
if (parts.length == 2) { final parts = data.split('|');
if (parts.length == 4) {
final ip = parts[0];
final port = parts[1];
final sessionId = parts[2];
final pin = parts[3];
final hostAddress = '$ip:$port';
_scannerController?.stop(); _scannerController?.stop();
setState(() { setState(() {
_errorMessage = null; _errorMessage = null;
_isConnecting = true; _isConnecting = true;
}); });
// Connect directly instead of going through _connect() which requires Form validation // Connect directly instead of going through _connect() which requires Form validation
_connectWithCredentials(parts[0], parts[1]); _connectWithCredentials(sessionId, pin, hostAddress);
} else { } else {
setState(() { setState(() {
_errorMessage = 'Invalid QR code format'; _errorMessage = 'Invalid QR code format - expected 4 parts (ip|port|sessionId|pin)';
}); });
} }
} }
Future<void> _connectWithCredentials(String sessionId, String pin) async { Future<void> _connectWithCredentials(String sessionId, String pin, String hostAddress) async {
try { try {
final provider = context.read<CompanionRemoteProvider>(); final provider = context.read<CompanionRemoteProvider>();
await provider.joinSession(sessionId.trim().toUpperCase(), pin.trim()); await provider.joinSession(sessionId.trim().toUpperCase(), pin.trim(), hostAddress.trim());
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -472,6 +485,33 @@ class _PairingScreenState extends State<PairingScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
TextFormField(
controller: _hostAddressController,
decoration: InputDecoration(
labelText: 'Host Address',
hintText: '192.168.1.100:48632',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.computer),
suffixIcon: IconButton(
icon: const Icon(Icons.paste),
onPressed: () => _pasteFromClipboard(_hostAddressController),
tooltip: 'Paste',
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter host address';
}
// Validate IP:port format
final parts = value.split(':');
if (parts.length != 2) {
return 'Format must be IP:port (e.g., 192.168.1.100:48632)';
}
return null;
},
enabled: !_isConnecting,
),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: _sessionIdController, controller: _sessionIdController,
decoration: InputDecoration( decoration: InputDecoration(
+5 -11
View File
@@ -1129,7 +1129,9 @@ class _DiscoverScreenState extends State<DiscoverScreen>
onKeyEvent: isDesktop ? _handleCompanionRemoteKeyEvent : null, onKeyEvent: isDesktop ? _handleCompanionRemoteKeyEvent : null,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDesktop && _isCompanionRemoteFocused ? Colors.white.withValues(alpha: 0.2) : Colors.transparent, color: isDesktop && _isCompanionRemoteFocused
? Colors.white.withValues(alpha: 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Stack( child: Stack(
@@ -1144,10 +1146,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
if (isDesktop) { if (isDesktop) {
RemoteSessionDialog.show(context); RemoteSessionDialog.show(context);
} else { } else {
Navigator.push( Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen()));
context,
MaterialPageRoute(builder: (context) => MobileRemoteScreen()),
);
} }
}, },
tooltip: 'Companion Remote', tooltip: 'Companion Remote',
@@ -1372,12 +1371,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Overlaid app bar — excluded from default focus traversal so that // Overlaid app bar — excluded from default focus traversal so that
// initial/tab-switch focus lands on content (hero/hubs), not the toolbar. // initial/tab-switch focus lands on content (hero/hubs), not the toolbar.
// Toolbar buttons are still reachable via explicit UP from hero section. // Toolbar buttons are still reachable via explicit UP from hero section.
Positioned( Positioned(top: 0, left: 0, right: 0, child: ExcludeFocusTraversal(child: _buildOverlaidAppBar())),
top: 0,
left: 0,
right: 0,
child: ExcludeFocusTraversal(child: _buildOverlaidAppBar()),
),
], ],
), ),
); );
+1 -4
View File
@@ -816,10 +816,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
: const Text('Control a desktop device'), : const Text('Control a desktop device'),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () { onTap: () {
Navigator.push( Navigator.push(context, MaterialPageRoute(builder: (context) => const MobileRemoteScreen()));
context,
MaterialPageRoute(builder: (context) => const MobileRemoteScreen()),
);
}, },
), ),
], ],
@@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:peerdart/peerdart.dart'; import 'package:web_socket_channel/io.dart';
import '../../models/companion_remote/remote_command.dart'; import '../../models/companion_remote/remote_command.dart';
import '../../models/companion_remote/remote_command_type.dart'; import '../../models/companion_remote/remote_command_type.dart';
@@ -15,6 +17,8 @@ enum RemotePeerErrorType {
serverError, serverError,
timeout, timeout,
invalidSession, invalidSession,
authFailed,
networkError,
unknown, unknown,
} }
@@ -23,22 +27,24 @@ class RemotePeerError {
final String message; final String message;
final dynamic originalError; final dynamic originalError;
const RemotePeerError({ const RemotePeerError({required this.type, required this.message, this.originalError});
required this.type,
required this.message,
this.originalError,
});
@override @override
String toString() => 'RemotePeerError($type): $message'; String toString() => 'RemotePeerError($type): $message';
} }
class CompanionRemotePeerService { class CompanionRemotePeerService {
Peer? _peer; // Server-side (host) fields
DataConnection? _connection; HttpServer? _server;
WebSocket? _clientSocket;
// Client-side (remote) fields
IOWebSocketChannel? _channel;
String? _sessionId; String? _sessionId;
String? _pin; String? _pin;
String? _myPeerId; String? _myPeerId;
String? _hostAddress; // Format: "ip:port"
RemoteSessionRole? _role; RemoteSessionRole? _role;
final _commandReceivedController = StreamController<RemoteCommand>.broadcast(); final _commandReceivedController = StreamController<RemoteCommand>.broadcast();
@@ -47,9 +53,6 @@ class CompanionRemotePeerService {
final _errorController = StreamController<RemotePeerError>.broadcast(); final _errorController = StreamController<RemotePeerError>.broadcast();
final _connectionStateController = StreamController<RemoteSessionStatus>.broadcast(); final _connectionStateController = StreamController<RemoteSessionStatus>.broadcast();
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 3;
Timer? _reconnectTimer;
Timer? _pingTimer; Timer? _pingTimer;
Stream<RemoteCommand> get onCommandReceived => _commandReceivedController.stream; Stream<RemoteCommand> get onCommandReceived => _commandReceivedController.stream;
@@ -61,9 +64,10 @@ class CompanionRemotePeerService {
String? get sessionId => _sessionId; String? get sessionId => _sessionId;
String? get pin => _pin; String? get pin => _pin;
String? get myPeerId => _myPeerId; String? get myPeerId => _myPeerId;
String? get hostAddress => _hostAddress;
RemoteSessionRole? get role => _role; RemoteSessionRole? get role => _role;
bool get isHost => _role == RemoteSessionRole.host; bool get isHost => _role == RemoteSessionRole.host;
bool get isConnected => _connection != null; bool get isConnected => _clientSocket != null || (_channel != null && _channel?.closeCode == null);
String _generateSessionId() { String _generateSessionId() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@@ -76,263 +80,368 @@ class CompanionRemotePeerService {
return List.generate(6, (index) => random.nextInt(10).toString()).join(); return List.generate(6, (index) => random.nextInt(10).toString()).join();
} }
void _attachCommonPeerListeners({ Future<String> _getLocalIpAddress() async {
required Completer completer, try {
required RemotePeerErrorType errorType, final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4);
required String errorMessage,
}) {
_peer!.on('disconnected').listen((_) {
appLogger.w('CompanionRemote: Peer disconnected from server');
_handleDisconnectedFromServer();
});
_peer!.on('close').listen((_) { // Prefer WiFi interface, then any non-loopback
appLogger.d('CompanionRemote: Peer closed'); for (final interface in interfaces) {
_connectionStateController.add(RemoteSessionStatus.disconnected); // Skip loopback
}); if (interface.name.toLowerCase().contains('lo')) continue;
_peer!.on('error').listen((error) { for (final addr in interface.addresses) {
appLogger.e('CompanionRemote: Peer error', error: error); if (!addr.isLoopback && addr.type == InternetAddressType.IPv4) {
_errorController.add( // Prefer names that suggest WiFi/Ethernet
RemotePeerError( if (interface.name.toLowerCase().contains('en') ||
type: errorType, interface.name.toLowerCase().contains('wl') ||
message: '$errorMessage: $error', interface.name.toLowerCase().contains('eth')) {
originalError: error, return addr.address;
), }
); }
if (!completer.isCompleted) { }
completer.completeError(error);
} }
});
// Fallback: return any non-loopback IPv4
for (final interface in interfaces) {
for (final addr in interface.addresses) {
if (!addr.isLoopback && addr.type == InternetAddressType.IPv4) {
return addr.address;
}
}
}
throw const RemotePeerError(type: RemotePeerErrorType.networkError, message: 'No network interface found');
} catch (e) {
appLogger.e('CompanionRemote: Failed to get local IP', error: e);
rethrow;
}
} }
Future<({String sessionId, String pin})> createSession(String deviceName, String platform) async { Future<({String sessionId, String pin, String address})> createSession(String deviceName, String platform) async {
if (_peer != null) { if (_server != null) {
await disconnect(); await disconnect();
} }
_role = RemoteSessionRole.host; _role = RemoteSessionRole.host;
_sessionId = _generateSessionId(); _sessionId = _generateSessionId();
_pin = _generatePin(); _pin = _generatePin();
_reconnectAttempts = 0; _myPeerId = 'host-$_sessionId';
final completer = Completer<({String sessionId, String pin})>();
try { try {
_peer = Peer(id: 'cr-$_sessionId-$_pin'); // Try preferred port first, fallback to OS-assigned port
const int preferredPort = 48632;
_peer!.on('open').listen((id) { try {
_myPeerId = id as String; _server = await HttpServer.bind(InternetAddress.anyIPv4, preferredPort);
appLogger.d('CompanionRemote: Host peer opened with ID: $_myPeerId'); appLogger.d('CompanionRemote: Server bound to port $preferredPort');
_connectionStateController.add(RemoteSessionStatus.connected); } catch (e) {
if (!completer.isCompleted) { appLogger.w('CompanionRemote: Port $preferredPort occupied, using random port');
completer.complete((sessionId: _sessionId!, pin: _pin!)); _server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
}
final localIp = await _getLocalIpAddress();
final port = _server!.port;
_hostAddress = '$localIp:$port';
appLogger.d('CompanionRemote: Host server started at $_hostAddress');
// Listen for WebSocket connections
_server!.listen((HttpRequest request) async {
if (request.uri.path == '/ws') {
try {
final socket = await WebSocketTransformer.upgrade(request);
_handleNewWebSocketConnection(socket, deviceName, platform);
} catch (e) {
appLogger.e('CompanionRemote: Failed to upgrade WebSocket', error: e);
}
} else {
request.response.statusCode = HttpStatus.notFound;
request.response.close();
} }
}); });
_peer!.on('connection').listen((conn) { _connectionStateController.add(RemoteSessionStatus.connected);
final dataConn = conn as DataConnection;
_handleNewConnection(dataConn, deviceName, platform);
});
_attachCommonPeerListeners( return (sessionId: _sessionId!, pin: _pin!, address: _hostAddress!);
completer: completer,
errorType: RemotePeerErrorType.serverError,
errorMessage: 'Server error',
);
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to create peer', error: e); appLogger.e('CompanionRemote: Failed to create server', error: e);
if (!completer.isCompleted) { _errorController.add(
completer.completeError(e); RemotePeerError(
} type: RemotePeerErrorType.serverError,
message: 'Failed to create server: $e',
originalError: e,
),
);
rethrow;
} }
}
return completer.future.timeout( void _handleNewWebSocketConnection(WebSocket socket, String hostDeviceName, String hostPlatform) {
const Duration(seconds: 10), appLogger.d('CompanionRemote: New WebSocket connection');
onTimeout: () {
throw RemotePeerError( bool isAuthenticated = false;
type: RemotePeerErrorType.timeout, Timer? authTimeout;
message: 'Timed out creating session',
// Authentication timeout
authTimeout = Timer(const Duration(seconds: 10), () {
if (!isAuthenticated) {
appLogger.w('CompanionRemote: Authentication timeout');
socket.close(4001, 'Authentication timeout');
}
});
socket.listen(
(data) {
try {
final json = jsonDecode(data as String) as Map<String, dynamic>;
if (!isAuthenticated) {
// First message must be authentication
if (json['type'] == 'auth') {
final sessionId = json['sessionId'] as String?;
final pin = json['pin'] as String?;
final deviceName = json['deviceName'] as String?;
final platform = json['platform'] as String?;
if (sessionId == _sessionId && pin == _pin) {
isAuthenticated = true;
authTimeout?.cancel();
// Close existing client if present
if (_clientSocket != null) {
appLogger.d('CompanionRemote: Replacing existing client connection');
_clientSocket!.close(4004, 'Replaced by new connection');
}
_clientSocket = socket;
appLogger.d('CompanionRemote: Client authenticated: $deviceName ($platform)');
// Send auth success
socket.add(jsonEncode({'type': 'authSuccess'}));
// Notify connection
final device = RemoteDevice(
id: 'remote-client',
name: deviceName ?? 'Unknown Device',
platform: platform ?? 'unknown',
);
_deviceConnectedController.add(device);
_connectionStateController.add(RemoteSessionStatus.connected);
// Send device info
sendDeviceInfo(hostDeviceName, hostPlatform);
// Note: Client sends keepalive pings, host only responds with pongs
} else {
appLogger.w('CompanionRemote: Invalid credentials');
socket.add(jsonEncode({'type': 'authFailed', 'message': 'Invalid session ID or PIN'}));
socket.close(4003, 'Invalid credentials');
}
} else {
appLogger.w('CompanionRemote: Expected auth, got ${json['type']}');
socket.close(4002, 'Authentication required');
}
} else {
// Handle regular commands
final command = RemoteCommand.fromJson(json);
appLogger.d('CompanionRemote: Received command: ${command.type}');
// Send acknowledgment for non-ping/pong/ack commands
if (command.type != RemoteCommandType.ping &&
command.type != RemoteCommandType.pong &&
command.type != RemoteCommandType.ack &&
command.type != RemoteCommandType.deviceInfo) {
final ackCommand = RemoteCommand(
type: RemoteCommandType.ack,
deviceId: _myPeerId ?? 'unknown',
deviceName: hostDeviceName,
data: {'originalCommand': command.type.toString()},
);
socket.add(jsonEncode(ackCommand.toJson()));
}
_commandReceivedController.add(command);
if (command.type == RemoteCommandType.ping) {
_sendPong(hostDeviceName, hostPlatform);
}
}
} catch (e) {
appLogger.e('CompanionRemote: Failed to process message', error: e);
}
},
onDone: () {
authTimeout?.cancel();
appLogger.d('CompanionRemote: WebSocket connection closed');
if (isAuthenticated) {
_clientSocket = null;
_deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected);
_stopPingTimer();
}
},
onError: (error) {
authTimeout?.cancel();
appLogger.e('CompanionRemote: WebSocket error', error: error);
_errorController.add(
RemotePeerError(
type: RemotePeerErrorType.dataChannelError,
message: 'WebSocket error: $error',
originalError: error,
),
); );
}, },
); );
} }
Future<void> joinSession( Future<void> joinSession(String sessionId, String pin, String deviceName, String platform, String hostAddress) async {
String sessionId, if (_channel != null) {
String pin,
String deviceName,
String platform,
) async {
if (_peer != null) {
await disconnect(); await disconnect();
} }
_role = RemoteSessionRole.remote; _role = RemoteSessionRole.remote;
_sessionId = sessionId.toUpperCase(); _sessionId = sessionId.toUpperCase();
_pin = pin; _pin = pin;
_reconnectAttempts = 0; _hostAddress = hostAddress;
_myPeerId = 'remote-${Random().nextInt(99999)}';
final completer = Completer<void>(); final completer = Completer<void>();
try { try {
_peer = Peer(); final url = 'ws://$hostAddress/ws';
appLogger.d('CompanionRemote: Connecting to $url');
_peer!.on('open').listen((id) { _connectionStateController.add(RemoteSessionStatus.connecting);
_myPeerId = id as String;
appLogger.d('CompanionRemote: Remote peer opened with ID: $_myPeerId');
final hostPeerId = 'cr-$_sessionId-$_pin'; _channel = IOWebSocketChannel.connect(Uri.parse(url));
appLogger.d('CompanionRemote: Connecting to host: $hostPeerId');
_connectionStateController.add(RemoteSessionStatus.connecting); // Send authentication message
final authMessage = jsonEncode({
final conn = _peer!.connect(hostPeerId, options: PeerConnectOption(reliable: true)); 'type': 'auth',
_handleNewConnection(conn, deviceName, platform, isOutgoing: true, completer: completer); 'sessionId': _sessionId,
'pin': _pin,
'deviceName': deviceName,
'platform': platform,
}); });
_channel!.sink.add(authMessage);
_attachCommonPeerListeners( // Listen for messages
completer: completer, _channel!.stream.listen(
errorType: RemotePeerErrorType.connectionFailed, (data) {
errorMessage: 'Failed to connect to session', try {
final json = jsonDecode(data as String) as Map<String, dynamic>;
final messageType = json['type'] as String?;
if (messageType == 'authSuccess') {
appLogger.d('CompanionRemote: Authentication successful');
if (!completer.isCompleted) {
completer.complete();
}
final device = RemoteDevice(id: 'host', name: 'Desktop', platform: 'desktop');
_deviceConnectedController.add(device);
_connectionStateController.add(RemoteSessionStatus.connected);
// Send device info
sendDeviceInfo(deviceName, platform);
// Start ping timer
_startPingTimer();
} else if (messageType == 'authFailed') {
final message = json['message'] as String? ?? 'Authentication failed';
appLogger.w('CompanionRemote: $message');
if (!completer.isCompleted) {
completer.completeError(RemotePeerError(type: RemotePeerErrorType.authFailed, message: message));
}
_errorController.add(RemotePeerError(type: RemotePeerErrorType.authFailed, message: message));
_connectionStateController.add(RemoteSessionStatus.error);
} else {
// Regular command
final command = RemoteCommand.fromJson(json);
appLogger.d('CompanionRemote: Received command: ${command.type}');
// Send acknowledgment for non-ping/pong/ack commands
if (command.type != RemoteCommandType.ping &&
command.type != RemoteCommandType.pong &&
command.type != RemoteCommandType.ack &&
command.type != RemoteCommandType.deviceInfo) {
final ackCommand = RemoteCommand(
type: RemoteCommandType.ack,
deviceId: _myPeerId ?? 'unknown',
deviceName: deviceName,
data: {'originalCommand': command.type.toString()},
);
_channel!.sink.add(jsonEncode(ackCommand.toJson()));
}
_commandReceivedController.add(command);
if (command.type == RemoteCommandType.ping) {
_sendPong(deviceName, platform);
}
}
} catch (e) {
appLogger.e('CompanionRemote: Failed to parse message', error: e);
}
},
onDone: () {
appLogger.d('CompanionRemote: Connection closed');
_deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected);
_stopPingTimer();
// Reconnection is handled by CompanionRemoteProvider
},
onError: (error) {
appLogger.e('CompanionRemote: Connection error', error: error);
if (!completer.isCompleted) {
completer.completeError(error);
}
_errorController.add(
RemotePeerError(
type: RemotePeerErrorType.connectionFailed,
message: 'Connection error: $error',
originalError: error,
),
);
_connectionStateController.add(RemoteSessionStatus.error);
},
); );
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to create peer for joining', error: e); appLogger.e('CompanionRemote: Failed to connect', error: e);
if (!completer.isCompleted) { if (!completer.isCompleted) {
completer.completeError(e); completer.completeError(e);
} }
_errorController.add(
RemotePeerError(type: RemotePeerErrorType.connectionFailed, message: 'Failed to connect: $e', originalError: e),
);
} }
return completer.future.timeout( return completer.future.timeout(
const Duration(seconds: 15), const Duration(seconds: 15),
onTimeout: () { onTimeout: () async {
throw RemotePeerError( // Clean up channel on timeout
type: RemotePeerErrorType.timeout, if (_channel != null) {
message: 'Timed out joining session', await _channel!.sink.close();
); _channel = null;
}
throw const RemotePeerError(type: RemotePeerErrorType.timeout, message: 'Timed out joining session');
}, },
); );
} }
void _handleNewConnection(
DataConnection conn,
String deviceName,
String platform, {
bool isOutgoing = false,
Completer<void>? completer,
}) {
final peerId = conn.peer;
appLogger.d('CompanionRemote: New connection ${isOutgoing ? "to" : "from"}: $peerId');
conn.on('open').listen((_) {
appLogger.d('CompanionRemote: Data channel opened with: $peerId');
_connection = conn;
_connectionStateController.add(RemoteSessionStatus.connected);
final device = RemoteDevice(
id: peerId,
name: deviceName,
platform: platform,
);
_deviceConnectedController.add(device);
_startPingTimer();
sendDeviceInfo(deviceName, platform);
if (completer != null && !completer.isCompleted) {
completer.complete();
}
});
conn.on('data').listen((data) {
try {
final json = data as Map<String, dynamic>;
final command = RemoteCommand.fromJson(json);
appLogger.d('CompanionRemote: Received command: ${command.type} from $peerId');
// Send acknowledgment for non-ping/pong/ack commands
if (command.type != RemoteCommandType.ping &&
command.type != RemoteCommandType.pong &&
command.type != RemoteCommandType.ack &&
command.type != RemoteCommandType.deviceInfo) {
final ackCommand = RemoteCommand(
type: RemoteCommandType.ack,
deviceId: _myPeerId ?? 'unknown',
deviceName: deviceName,
data: {'originalCommand': command.type.toString()},
);
_connection?.send(ackCommand.toJson());
}
_commandReceivedController.add(command);
if (command.type == RemoteCommandType.ping) {
_sendPong(deviceName, platform);
} else if (command.type == RemoteCommandType.ack) {
appLogger.d('CompanionRemote: Received ACK for: ${json['data']?['originalCommand']}');
}
} catch (e) {
appLogger.e('CompanionRemote: Failed to parse command', error: e);
}
});
conn.on('close').listen((_) {
appLogger.d('CompanionRemote: Connection closed with: $peerId');
_connection = null;
_deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected);
_stopPingTimer();
});
conn.on('error').listen((error) {
appLogger.e('CompanionRemote: Connection error with $peerId', error: error);
_errorController.add(
RemotePeerError(
type: RemotePeerErrorType.dataChannelError,
message: 'Connection error with peer: $error',
originalError: error,
),
);
_connectionStateController.add(RemoteSessionStatus.error);
});
}
void _handleDisconnectedFromServer() {
if (_reconnectAttempts < _maxReconnectAttempts) {
_reconnectAttempts++;
final delay = Duration(seconds: _reconnectAttempts * 2);
appLogger.d(
'CompanionRemote: Attempting reconnect $_reconnectAttempts/$_maxReconnectAttempts in ${delay.inSeconds}s',
);
_reconnectTimer?.cancel();
_reconnectTimer = Timer(delay, () {
_peer?.reconnect();
});
} else {
appLogger.e('CompanionRemote: Max reconnect attempts reached');
_errorController.add(
const RemotePeerError(
type: RemotePeerErrorType.connectionFailed,
message: 'Lost connection to server after multiple reconnect attempts',
),
);
_connectionStateController.add(RemoteSessionStatus.error);
}
}
void _startPingTimer() { void _startPingTimer() {
_stopPingTimer(); _stopPingTimer();
_pingTimer = Timer.periodic(const Duration(seconds: 5), (_) { _pingTimer = Timer.periodic(const Duration(seconds: 5), (_) {
if (_connection != null) { if (isConnected) {
sendCommand(RemoteCommand( sendCommand(RemoteCommand(type: RemoteCommandType.ping, deviceId: _myPeerId ?? 'unknown', deviceName: 'local'));
type: RemoteCommandType.ping,
deviceId: _myPeerId ?? 'unknown',
deviceName: 'local',
));
} }
}); });
} }
@@ -343,36 +452,40 @@ class CompanionRemotePeerService {
} }
void _sendPong(String deviceName, String platform) { void _sendPong(String deviceName, String platform) {
sendCommand(RemoteCommand( sendCommand(
type: RemoteCommandType.pong, RemoteCommand(
deviceId: _myPeerId ?? 'unknown', type: RemoteCommandType.pong,
deviceName: deviceName, deviceId: _myPeerId ?? 'unknown',
data: {'platform': platform}, deviceName: deviceName,
)); data: {'platform': platform},
),
);
} }
void sendDeviceInfo(String deviceName, String platform) { void sendDeviceInfo(String deviceName, String platform) {
sendCommand(RemoteCommand( sendCommand(
type: RemoteCommandType.deviceInfo, RemoteCommand(
deviceId: _myPeerId ?? 'unknown', type: RemoteCommandType.deviceInfo,
deviceName: deviceName, deviceId: _myPeerId ?? 'unknown',
data: { deviceName: deviceName,
'platform': platform, data: {'platform': platform, 'role': _role?.name},
'role': _role?.name, ),
}, );
));
} }
void sendCommand(RemoteCommand command) { void sendCommand(RemoteCommand command) {
if (_connection == null) {
appLogger.w('CompanionRemote: No connection to send command');
return;
}
try { try {
final json = command.toJson(); final json = jsonEncode(command.toJson());
_connection!.send(json);
appLogger.d('CompanionRemote: Sent command: ${command.type}'); if (_role == RemoteSessionRole.host && _clientSocket != null) {
_clientSocket!.add(json);
appLogger.d('CompanionRemote: Sent command (host): ${command.type}');
} else if (_role == RemoteSessionRole.remote && _channel != null) {
_channel!.sink.add(json);
appLogger.d('CompanionRemote: Sent command (remote): ${command.type}');
} else {
appLogger.w('CompanionRemote: No connection to send command');
}
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to send command', error: e); appLogger.e('CompanionRemote: Failed to send command', error: e);
_errorController.add( _errorController.add(
@@ -389,29 +502,37 @@ class CompanionRemotePeerService {
appLogger.d('CompanionRemote: Disconnecting'); appLogger.d('CompanionRemote: Disconnecting');
_stopPingTimer(); _stopPingTimer();
_reconnectTimer?.cancel();
_connection?.close(); if (_clientSocket != null) {
_connection = null; await _clientSocket!.close();
_clientSocket = null;
}
_peer?.dispose(); if (_channel != null) {
_peer = null; await _channel!.sink.close();
_channel = null;
}
if (_server != null) {
await _server!.close();
_server = null;
}
_sessionId = null; _sessionId = null;
_pin = null; _pin = null;
_myPeerId = null; _myPeerId = null;
_hostAddress = null;
_role = null; _role = null;
_reconnectAttempts = 0;
_connectionStateController.add(RemoteSessionStatus.disconnected); _connectionStateController.add(RemoteSessionStatus.disconnected);
} }
void dispose() { Future<void> dispose() async {
disconnect(); await disconnect();
_commandReceivedController.close(); await _commandReceivedController.close();
_deviceConnectedController.close(); await _deviceConnectedController.close();
_deviceDisconnectedController.close(); await _deviceDisconnectedController.close();
_errorController.close(); await _errorController.close();
_connectionStateController.close(); await _connectionStateController.close();
} }
} }
-1
View File
@@ -343,5 +343,4 @@ class GamepadService {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
} }
} }
} }
@@ -24,6 +24,7 @@ class RemoteSessionDialog extends StatefulWidget {
class _RemoteSessionDialogState extends State<RemoteSessionDialog> { class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
bool _isCreatingSession = false; bool _isCreatingSession = false;
String? _errorMessage; String? _errorMessage;
String? _hostAddress; // Format: "ip:port"
@override @override
void initState() { void initState() {
@@ -35,14 +36,16 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
setState(() { setState(() {
_isCreatingSession = true; _isCreatingSession = true;
_errorMessage = null; _errorMessage = null;
_hostAddress = null;
}); });
try { try {
final provider = context.read<CompanionRemoteProvider>(); final provider = context.read<CompanionRemoteProvider>();
await provider.createSession(); final result = await provider.createSession();
setState(() { setState(() {
_isCreatingSession = false; _isCreatingSession = false;
_hostAddress = result.address;
}); });
} catch (e) { } catch (e) {
appLogger.e('Failed to create companion remote session', error: e); appLogger.e('Failed to create companion remote session', error: e);
@@ -55,12 +58,9 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
void _copyToClipboard(String text, String label) { void _copyToClipboard(String text, String label) {
Clipboard.setData(ClipboardData(text: text)); Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text('$label copied to clipboard'), ).showSnackBar(SnackBar(content: Text('$label copied to clipboard'), duration: const Duration(seconds: 2)));
duration: const Duration(seconds: 2),
),
);
} }
@override @override
@@ -76,10 +76,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text('Creating remote session...', style: Theme.of(context).textTheme.titleMedium),
'Creating remote session...',
style: Theme.of(context).textTheme.titleMedium,
),
], ],
), ),
), ),
@@ -95,40 +92,32 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
children: [ children: [
const Text('Failed to create remote session:'), const Text('Failed to create remote session:'),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(_errorMessage!, style: const TextStyle(fontFamily: 'monospace')),
_errorMessage!,
style: const TextStyle(fontFamily: 'monospace'),
),
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Close')),
onPressed: () => Navigator.of(context).pop(), TextButton(onPressed: _createSession, child: const Text('Retry')),
child: const Text('Close'),
),
TextButton(
onPressed: _createSession,
child: const Text('Retry'),
),
], ],
); );
} }
final session = provider.session; final session = provider.session;
if (session == null) { if (session == null || _hostAddress == null) {
return AlertDialog( return AlertDialog(
title: const Text('Error'), title: const Text('Error'),
content: const Text('No session available'), content: const Text('No session available'),
actions: [ actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Close'))],
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
); );
} }
final qrData = '${session.sessionId}:${session.pin}'; // Parse IP and port from hostAddress
final addressParts = _hostAddress!.split(':');
final ip = addressParts[0];
final port = addressParts[1];
// New QR format: ip|port|sessionId|pin (using pipe separator)
final qrData = '$ip|$port|${session.sessionId}|${session.pin}';
return Dialog( return Dialog(
child: ConstrainedBox( child: ConstrainedBox(
@@ -147,45 +136,32 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text('Companion Remote', style: Theme.of(context).textTheme.headlineSmall),
'Companion Remote',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
session.connectedDevice != null session.connectedDevice != null
? 'Connected to ${session.connectedDevice!.name}' ? 'Connected to ${session.connectedDevice!.name}'
: 'Waiting for connection...', : 'Waiting for connection...',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: session.connectedDevice != null color: session.connectedDevice != null
? Colors.green ? Colors.green
: Theme.of(context).textTheme.bodySmall?.color, : Theme.of(context).textTheme.bodySmall?.color,
), ),
), ),
], ],
), ),
), ),
IconButton( IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()),
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
if (session.connectedDevice == null) ...[ if (session.connectedDevice == null) ...[
Text( Text('Scan QR Code', style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center),
'Scan QR Code',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Center( Center(
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: QrImageView( child: QrImageView(
data: qrData, data: qrData,
version: QrVersions.auto, version: QrVersions.auto,
@@ -203,6 +179,13 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildCodeCard(
context,
'Host Address',
_hostAddress!,
onCopy: () => _copyToClipboard(_hostAddress!, 'Host Address'),
),
const SizedBox(height: 12),
_buildCodeCard( _buildCodeCard(
context, context,
'Session ID', 'Session ID',
@@ -210,37 +193,19 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
onCopy: () => _copyToClipboard(session.sessionId, 'Session ID'), onCopy: () => _copyToClipboard(session.sessionId, 'Session ID'),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildCodeCard( _buildCodeCard(context, 'PIN', session.pin, onCopy: () => _copyToClipboard(session.pin, 'PIN')),
context,
'PIN',
session.pin,
onCopy: () => _copyToClipboard(session.pin, 'PIN'),
),
] else ...[ ] else ...[
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
const Icon( const Icon(Icons.check_circle, color: Colors.green, size: 48),
Icons.check_circle,
color: Colors.green,
size: 48,
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text('Connected', style: Theme.of(context).textTheme.titleLarge),
'Connected',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(session.connectedDevice!.name, style: Theme.of(context).textTheme.bodyLarge),
session.connectedDevice!.name, Text(session.connectedDevice!.platform, style: Theme.of(context).textTheme.bodySmall),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
session.connectedDevice!.platform,
style: Theme.of(context).textTheme.bodySmall,
),
], ],
), ),
), ),
@@ -276,10 +241,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
child: const Text('Disconnect'), child: const Text('Disconnect'),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
FilledButton( FilledButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Minimize')),
onPressed: () => Navigator.of(context).pop(),
child: const Text('Minimize'),
),
], ],
), ),
], ],
@@ -291,12 +253,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
); );
} }
Widget _buildCodeCard( Widget _buildCodeCard(BuildContext context, String label, String code, {VoidCallback? onCopy}) {
BuildContext context,
String label,
String code, {
VoidCallback? onCopy,
}) {
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@@ -306,26 +263,16 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(label, style: Theme.of(context).textTheme.bodySmall),
label,
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
code, code,
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(fontFamily: 'monospace', letterSpacing: 2),
fontFamily: 'monospace',
letterSpacing: 2,
),
), ),
], ],
), ),
), ),
IconButton( IconButton(icon: const Icon(Icons.copy), onPressed: onCopy, tooltip: 'Copy to clipboard'),
icon: const Icon(Icons.copy),
onPressed: onCopy,
tooltip: 'Copy to clipboard',
),
], ],
), ),
), ),
@@ -6,7 +6,6 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <os_media_controls/os_media_controls_plugin.h> #include <os_media_controls/os_media_controls_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h> #include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
@@ -15,9 +14,6 @@
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
g_autoptr(FlPluginRegistrar) os_media_controls_registrar = g_autoptr(FlPluginRegistrar) os_media_controls_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "OsMediaControlsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "OsMediaControlsPlugin");
os_media_controls_plugin_register_with_registrar(os_media_controls_registrar); os_media_controls_plugin_register_with_registrar(os_media_controls_registrar);
-1
View File
@@ -3,7 +3,6 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_webrtc
os_media_controls os_media_controls
screen_retriever_linux screen_retriever_linux
sqlite3_flutter_libs sqlite3_flutter_libs
@@ -8,7 +8,6 @@ import Foundation
import connectivity_plus import connectivity_plus
import device_info_plus import device_info_plus
import file_picker import file_picker
import flutter_webrtc
import in_app_review import in_app_review
import mobile_scanner import mobile_scanner
import os_media_controls import os_media_controls
@@ -27,7 +26,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin")) OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin"))
-10
View File
@@ -5,9 +5,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- file_picker (0.0.1): - file_picker (0.0.1):
- FlutterMacOS - FlutterMacOS
- flutter_webrtc (1.2.0):
- FlutterMacOS
- WebRTC-SDK (= 137.7151.04)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- in_app_review (2.0.0): - in_app_review (2.0.0):
- FlutterMacOS - FlutterMacOS
@@ -59,7 +56,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (137.7151.04)
- window_manager (0.5.0): - window_manager (0.5.0):
- FlutterMacOS - FlutterMacOS
@@ -67,7 +63,6 @@ DEPENDENCIES:
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
@@ -86,7 +81,6 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- sqlite3 - sqlite3
- WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
connectivity_plus: connectivity_plus:
@@ -95,8 +89,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_picker: file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
flutter_webrtc:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
in_app_review: in_app_review:
@@ -130,7 +122,6 @@ SPEC CHECKSUMS:
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4 in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
@@ -145,7 +136,6 @@ SPEC CHECKSUMS:
universal_gamepad: 8922f1f238f62d6847de887228976d5b572b57da universal_gamepad: 8922f1f238f62d6847de887228976d5b572b57da
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
window_manager: b729e31d38fb04905235df9ea896128991cad99e window_manager: b729e31d38fb04905235df9ea896128991cad99e
PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d
-40
View File
@@ -297,14 +297,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
dart_webrtc:
dependency: transitive
description:
name: dart_webrtc
sha256: "4ed7b9fa9924e5a81eb39271e2c2356739dd1039d60a13b86ba6c5f448625086"
url: "https://pub.dev"
source: hosted
version: "1.7.0"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@@ -369,14 +361,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.3" version: "4.0.3"
events_emitter:
dependency: transitive
description:
name: events_emitter
sha256: a075477bdf9c8c0c31bb7c7b7bdd357b4486c34f30163119f96de4e7f54abeff
url: "https://pub.dev"
source: hosted
version: "0.5.2"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -493,14 +477,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_webrtc:
dependency: "direct overridden"
description:
name: flutter_webrtc
sha256: "0f86b518e9349e71a136a96e0ea11294cad8a8531b2bc9ae99e69df332ac898a"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -838,14 +814,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
peerdart:
dependency: "direct main"
description:
name: peerdart
sha256: "1d0db041d42194f42e57d8889d029fb8d107610bf1062b39f43f4651de547e3c"
url: "https://pub.dev"
source: hosted
version: "0.5.6"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -1451,14 +1419,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
webrtc_interface:
dependency: transitive
description:
name: webrtc_interface
sha256: ad0e5786b2acd3be72a3219ef1dde9e1cac071cf4604c685f11b61d63cdd6eb3
url: "https://pub.dev"
source: hosted
version: "1.4.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
-4
View File
@@ -49,10 +49,6 @@ dependencies:
dart_discord_presence: ^1.1.0 dart_discord_presence: ^1.1.0
flutter_svg: ^2.2.3 flutter_svg: ^2.2.3
mobile_scanner: ^6.0.2 mobile_scanner: ^6.0.2
peerdart: ^0.5.6
dependency_overrides:
flutter_webrtc: ^1.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -7,7 +7,6 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <os_media_controls/os_media_controls_plugin_c_api.h> #include <os_media_controls/os_media_controls_plugin_c_api.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
@@ -18,8 +17,6 @@
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar( ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
OsMediaControlsPluginCApiRegisterWithRegistrar( OsMediaControlsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi")); registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
-1
View File
@@ -4,7 +4,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus connectivity_plus
flutter_webrtc
os_media_controls os_media_controls
screen_retriever_windows screen_retriever_windows
sqlite3_flutter_libs sqlite3_flutter_libs