From 372767b14264038f31ae6f22e881cb2a0cd9d81e Mon Sep 17 00:00:00 2001 From: Matt Vogel Date: Mon, 9 Feb 2026 14:19:54 -0500 Subject: [PATCH 1/8] Add Companion Remote for mobile-to-desktop control --- android/app/src/main/AndroidManifest.xml | 3 + ios/Runner/Info.plist | 2 +- lib/focus/input_mode_tracker.dart | 5 + lib/main.dart | 2 + lib/mixins/refreshable.dart | 1 + .../companion_remote/remote_command.dart | 80 ++ .../companion_remote/remote_command_type.dart | 191 +++++ .../companion_remote/remote_session.dart | 151 ++++ .../companion_remote/trusted_device.dart | 67 ++ lib/providers/companion_remote_provider.dart | 542 +++++++++++++ .../mobile_remote_screen.dart | 731 ++++++++++++++++++ .../companion_remote/pairing_screen.dart | 653 ++++++++++++++++ lib/screens/discover_screen.dart | 151 +++- lib/screens/main_screen.dart | 81 ++ lib/screens/search_screen.dart | 6 + lib/screens/settings/settings_screen.dart | 53 ++ lib/screens/video_player_screen.dart | 66 ++ .../companion_remote_discovery_service.dart | 152 ++++ .../companion_remote_peer_service.dart | 435 +++++++++++ .../companion_remote_receiver.dart | 137 ++++ lib/services/gamepad_service.dart | 15 +- lib/utils/key_event_simulator.dart | 76 ++ .../remote_session_dialog.dart | 335 ++++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 + macos/Podfile.lock | 27 +- pubspec.lock | 288 ++++--- pubspec.yaml | 5 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 31 files changed, 4140 insertions(+), 130 deletions(-) create mode 100644 lib/models/companion_remote/remote_command.dart create mode 100644 lib/models/companion_remote/remote_command_type.dart create mode 100644 lib/models/companion_remote/remote_session.dart create mode 100644 lib/models/companion_remote/trusted_device.dart create mode 100644 lib/providers/companion_remote_provider.dart create mode 100644 lib/screens/companion_remote/mobile_remote_screen.dart create mode 100644 lib/screens/companion_remote/pairing_screen.dart create mode 100644 lib/services/companion_remote/companion_remote_discovery_service.dart create mode 100644 lib/services/companion_remote/companion_remote_peer_service.dart create mode 100644 lib/services/companion_remote/companion_remote_receiver.dart create mode 100644 lib/utils/key_event_simulator.dart create mode 100644 lib/widgets/companion_remote/remote_session_dialog.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e2828c5a..eaa47481 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index eae1b50b..5278ba84 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -40,7 +40,7 @@ NSLocalNetworkUsageDescription This app needs to connect to your Plex Media Server on your local network. NSCameraUsageDescription - Camera access is included by the WebRTC library used for Watch Together, but is not actively used by this app. + Camera is used to scan QR codes for Companion Remote pairing. UIApplicationSupportsIndirectInputEvents UIFileSharingEnabled diff --git a/lib/focus/input_mode_tracker.dart b/lib/focus/input_mode_tracker.dart index 8462b912..aeef05cd 100644 --- a/lib/focus/input_mode_tracker.dart +++ b/lib/focus/input_mode_tracker.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import '../utils/platform_detector.dart'; import '../services/gamepad_service.dart'; import 'dpad_navigator.dart'; +import '../services/companion_remote/companion_remote_receiver.dart'; /// Tracks whether the user is navigating via keyboard/d-pad or pointer (mouse/touch). /// @@ -57,12 +58,16 @@ class _InputModeTrackerState extends State { // Register callback for gamepad input to switch to keyboard mode GamepadService.onGamepadInput = () => _setMode(InputMode.keyboard); + + // Register callback for companion remote input to switch to keyboard mode + CompanionRemoteReceiver.onRemoteInput = () => _setMode(InputMode.keyboard); } @override void dispose() { HardwareKeyboard.instance.removeHandler(_handleKeyEvent); GamepadService.onGamepadInput = null; + CompanionRemoteReceiver.onRemoteInput = null; super.dispose(); } diff --git a/lib/main.dart b/lib/main.dart index a21a439a..d6f9fed7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,6 +25,7 @@ import 'providers/download_provider.dart'; import 'providers/offline_mode_provider.dart'; import 'providers/offline_watch_provider.dart'; import 'providers/shader_provider.dart'; +import 'providers/companion_remote_provider.dart'; import 'watch_together/watch_together.dart'; import 'services/multi_server_manager.dart'; import 'services/offline_watch_sync_service.dart'; @@ -307,6 +308,7 @@ class _MainAppState extends State with WidgetsBindingObserver { ChangeNotifierProvider(create: (context) => PlaybackStateProvider()), ChangeNotifierProvider(create: (context) => WatchTogetherProvider()), ChangeNotifierProvider(create: (context) => ShaderProvider()), + ChangeNotifierProvider(create: (context) => CompanionRemoteProvider()), ], child: Consumer( builder: (context, themeProvider, child) { diff --git a/lib/mixins/refreshable.dart b/lib/mixins/refreshable.dart index 709d6039..9a4056ca 100644 --- a/lib/mixins/refreshable.dart +++ b/lib/mixins/refreshable.dart @@ -15,6 +15,7 @@ mixin FocusableTab { /// Mixin for screens with focusable search input mixin SearchInputFocusable { void focusSearchInput(); + void setSearchQuery(String query); } /// Mixin for screens that can load a specific library by key diff --git a/lib/models/companion_remote/remote_command.dart b/lib/models/companion_remote/remote_command.dart new file mode 100644 index 00000000..aab9879a --- /dev/null +++ b/lib/models/companion_remote/remote_command.dart @@ -0,0 +1,80 @@ +import 'remote_command_type.dart'; + +class RemoteCommand { + final RemoteCommandType type; + final String deviceId; + final String deviceName; + final DateTime timestamp; + final Map? data; + + RemoteCommand({ + required this.type, + required this.deviceId, + required this.deviceName, + DateTime? timestamp, + this.data, + }) : timestamp = timestamp ?? DateTime.now(); + + Map toJson() { + return { + 'type': type.name, + 'deviceId': deviceId, + 'deviceName': deviceName, + 'timestamp': timestamp.toIso8601String(), + if (data != null) 'data': data, + }; + } + + factory RemoteCommand.fromJson(Map json) { + return RemoteCommand( + type: RemoteCommandType.values.firstWhere( + (e) => e.name == json['type'], + orElse: () => RemoteCommandType.ping, + ), + deviceId: json['deviceId'] as String, + deviceName: json['deviceName'] as String, + timestamp: DateTime.parse(json['timestamp'] as String), + data: json['data'] as Map?, + ); + } + + RemoteCommand copyWith({ + RemoteCommandType? type, + String? deviceId, + String? deviceName, + DateTime? timestamp, + Map? data, + }) { + return RemoteCommand( + type: type ?? this.type, + deviceId: deviceId ?? this.deviceId, + deviceName: deviceName ?? this.deviceName, + timestamp: timestamp ?? this.timestamp, + data: data ?? this.data, + ); + } + + @override + String toString() { + return 'RemoteCommand(type: ${type.name}, device: $deviceName, data: $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; + } +} diff --git a/lib/models/companion_remote/remote_command_type.dart b/lib/models/companion_remote/remote_command_type.dart new file mode 100644 index 00000000..ca5febb1 --- /dev/null +++ b/lib/models/companion_remote/remote_command_type.dart @@ -0,0 +1,191 @@ +enum RemoteCommandType { + // Navigation + dpadUp, + dpadDown, + dpadLeft, + dpadRight, + select, + back, + contextMenu, + + // Playback + play, + pause, + playPause, + stop, + seekForward, + seekBackward, + nextTrack, + previousTrack, + skipIntro, + skipCredits, + + // Volume + volumeUp, + volumeDown, + volumeMute, + volumeSet, + + // Tab Navigation + tabNext, + tabPrevious, + tabDiscover, + tabLibraries, + tabSearch, + tabDownloads, + tabSettings, + + // Quick Actions + home, + search, + subtitles, + audioTracks, + qualitySettings, + fullscreen, + + // Session Management + 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); + } +} diff --git a/lib/models/companion_remote/remote_session.dart b/lib/models/companion_remote/remote_session.dart new file mode 100644 index 00000000..012e92df --- /dev/null +++ b/lib/models/companion_remote/remote_session.dart @@ -0,0 +1,151 @@ +enum RemoteSessionRole { + host, + remote, +} + +enum RemoteSessionStatus { + disconnected, + connecting, + connected, + reconnecting, + error, +} + +class RemoteDevice { + final String id; + final String name; + final String platform; + final DateTime connectedAt; + final Map capabilities; + + RemoteDevice({ + required this.id, + required this.name, + required this.platform, + DateTime? connectedAt, + Map? capabilities, + }) : connectedAt = connectedAt ?? DateTime.now(), + capabilities = capabilities ?? {}; + + Map toJson() { + return { + 'id': id, + 'name': name, + 'platform': platform, + 'connectedAt': connectedAt.toIso8601String(), + 'capabilities': capabilities, + }; + } + + factory RemoteDevice.fromJson(Map json) { + return RemoteDevice( + id: json['id'] as String, + name: json['name'] as String, + platform: json['platform'] as String, + connectedAt: DateTime.parse(json['connectedAt'] as String), + capabilities: Map.from(json['capabilities'] as Map? ?? {}), + ); + } + + RemoteDevice copyWith({ + String? id, + String? name, + String? platform, + DateTime? connectedAt, + Map? capabilities, + }) { + return RemoteDevice( + id: id ?? this.id, + name: name ?? this.name, + platform: platform ?? this.platform, + connectedAt: connectedAt ?? this.connectedAt, + capabilities: capabilities ?? this.capabilities, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is RemoteDevice && other.id == id; + } + + @override + int get hashCode => id.hashCode; +} + +class RemoteSession { + final String sessionId; + final String pin; + final RemoteSessionRole role; + final RemoteSessionStatus status; + final RemoteDevice? connectedDevice; + final DateTime createdAt; + final String? errorMessage; + + RemoteSession({ + required this.sessionId, + required this.pin, + required this.role, + this.status = RemoteSessionStatus.disconnected, + this.connectedDevice, + DateTime? createdAt, + this.errorMessage, + }) : createdAt = createdAt ?? DateTime.now(); + + bool get isConnected => status == RemoteSessionStatus.connected; + bool get isHost => role == RemoteSessionRole.host; + bool get isRemote => role == RemoteSessionRole.remote; + + RemoteSession copyWith({ + String? sessionId, + String? pin, + RemoteSessionRole? role, + RemoteSessionStatus? status, + RemoteDevice? connectedDevice, + DateTime? createdAt, + String? errorMessage, + }) { + return RemoteSession( + sessionId: sessionId ?? this.sessionId, + pin: pin ?? this.pin, + role: role ?? this.role, + status: status ?? this.status, + connectedDevice: connectedDevice ?? this.connectedDevice, + createdAt: createdAt ?? this.createdAt, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + Map toJson() { + return { + 'sessionId': sessionId, + 'pin': pin, + 'role': role.name, + 'status': status.name, + 'connectedDevice': connectedDevice?.toJson(), + 'createdAt': createdAt.toIso8601String(), + 'errorMessage': errorMessage, + }; + } + + factory RemoteSession.fromJson(Map json) { + return RemoteSession( + sessionId: json['sessionId'] as String, + pin: json['pin'] as String, + role: RemoteSessionRole.values.firstWhere( + (e) => e.name == json['role'], + orElse: () => RemoteSessionRole.remote, + ), + status: RemoteSessionStatus.values.firstWhere( + (e) => e.name == json['status'], + orElse: () => RemoteSessionStatus.disconnected, + ), + connectedDevice: json['connectedDevice'] != null + ? RemoteDevice.fromJson(json['connectedDevice'] as Map) + : null, + createdAt: DateTime.parse(json['createdAt'] as String), + errorMessage: json['errorMessage'] as String?, + ); + } +} diff --git a/lib/models/companion_remote/trusted_device.dart b/lib/models/companion_remote/trusted_device.dart new file mode 100644 index 00000000..35c8a9a3 --- /dev/null +++ b/lib/models/companion_remote/trusted_device.dart @@ -0,0 +1,67 @@ +class TrustedDevice { + final String peerId; + final String deviceName; + final String platform; + final DateTime firstConnected; + final DateTime lastConnected; + final bool isApproved; + + TrustedDevice({ + required this.peerId, + required this.deviceName, + required this.platform, + DateTime? firstConnected, + DateTime? lastConnected, + this.isApproved = false, + }) : firstConnected = firstConnected ?? DateTime.now(), + lastConnected = lastConnected ?? DateTime.now(); + + Map toJson() { + return { + 'peerId': peerId, + 'deviceName': deviceName, + 'platform': platform, + 'firstConnected': firstConnected.toIso8601String(), + 'lastConnected': lastConnected.toIso8601String(), + 'isApproved': isApproved, + }; + } + + factory TrustedDevice.fromJson(Map json) { + return TrustedDevice( + peerId: json['peerId'] as String, + deviceName: json['deviceName'] as String, + platform: json['platform'] as String, + firstConnected: DateTime.parse(json['firstConnected'] as String), + lastConnected: DateTime.parse(json['lastConnected'] as String), + isApproved: json['isApproved'] as bool? ?? false, + ); + } + + TrustedDevice copyWith({ + String? peerId, + String? deviceName, + String? platform, + DateTime? firstConnected, + DateTime? lastConnected, + bool? isApproved, + }) { + return TrustedDevice( + peerId: peerId ?? this.peerId, + deviceName: deviceName ?? this.deviceName, + platform: platform ?? this.platform, + firstConnected: firstConnected ?? this.firstConnected, + lastConnected: lastConnected ?? this.lastConnected, + isApproved: isApproved ?? this.isApproved, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is TrustedDevice && other.peerId == peerId; + } + + @override + int get hashCode => peerId.hashCode; +} diff --git a/lib/providers/companion_remote_provider.dart b/lib/providers/companion_remote_provider.dart new file mode 100644 index 00000000..98c4b4dd --- /dev/null +++ b/lib/providers/companion_remote_provider.dart @@ -0,0 +1,542 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +import '../models/companion_remote/remote_command.dart'; +import '../models/companion_remote/remote_command_type.dart'; +import '../models/companion_remote/remote_session.dart'; +import '../models/companion_remote/trusted_device.dart'; +import '../services/companion_remote/companion_remote_peer_service.dart'; +import '../services/companion_remote/companion_remote_discovery_service.dart'; +import '../services/storage_service.dart'; +import '../utils/app_logger.dart'; + +typedef CommandReceivedCallback = void Function(RemoteCommand command); +typedef DeviceApprovalCallback = Future Function(RemoteDevice device); + +class CompanionRemoteProvider with ChangeNotifier { + RemoteSession? _session; + CompanionRemotePeerService? _peerService; + CompanionRemoteDiscoveryService? _discoveryService; + String _deviceName = 'Unknown Device'; + String _platform = 'unknown'; + final List _trustedDevices = []; + final List _recentSessions = []; + + static const String _storageKey = 'companion_remote_trusted_devices'; + static const String _lastDeviceKey = 'companion_remote_last_device'; + static const int _maxReconnectAttempts = 5; + + Timer? _reconnectTimer; + int _reconnectAttempts = 0; + bool _intentionalDisconnect = false; + String? _lastSessionId; + String? _lastPin; + + int get reconnectAttempts => _reconnectAttempts; + + StreamSubscription? _commandSubscription; + StreamSubscription? _deviceConnectedSubscription; + StreamSubscription? _deviceDisconnectedSubscription; + StreamSubscription? _errorSubscription; + StreamSubscription? _statusSubscription; + + CommandReceivedCallback? onCommandReceived; + DeviceApprovalCallback? onDeviceApprovalRequired; + + bool get isInSession => _session != null && _session!.status != RemoteSessionStatus.disconnected; + bool get isHost => _session?.isHost ?? false; + bool get isRemote => _session?.isRemote ?? false; + bool get isConnected => _session?.isConnected ?? false; + RemoteSession? get session => _session; + RemoteSessionStatus get status => _session?.status ?? RemoteSessionStatus.disconnected; + String? get sessionId => _session?.sessionId; + String? get pin => _session?.pin; + RemoteDevice? get connectedDevice => _session?.connectedDevice; + List get trustedDevices => List.unmodifiable(_trustedDevices); + List get recentSessions => List.unmodifiable(_recentSessions); + + CompanionRemoteProvider() { + _initializeDeviceInfo(); + _loadTrustedDevices(); + } + + Future _initializeDeviceInfo() async { + final deviceInfo = DeviceInfoPlugin(); + + try { + if (Platform.isAndroid) { + final androidInfo = await deviceInfo.androidInfo; + _deviceName = '${androidInfo.brand} ${androidInfo.model}'; + _platform = 'Android'; + } else if (Platform.isIOS) { + final iosInfo = await deviceInfo.iosInfo; + _deviceName = iosInfo.name; + _platform = 'iOS'; + } else if (Platform.isMacOS) { + final macInfo = await deviceInfo.macOsInfo; + _deviceName = macInfo.computerName; + _platform = 'macOS'; + } else if (Platform.isWindows) { + final windowsInfo = await deviceInfo.windowsInfo; + _deviceName = windowsInfo.computerName; + _platform = 'Windows'; + } else if (Platform.isLinux) { + final linuxInfo = await deviceInfo.linuxInfo; + _deviceName = linuxInfo.name; + _platform = 'Linux'; + } + } catch (e) { + appLogger.e('CompanionRemote: Failed to get device info', error: e); + _deviceName = 'Unknown Device'; + _platform = Platform.operatingSystem; + } + + notifyListeners(); + } + + void _setupPeerServiceListeners() { + _commandSubscription = _peerService!.onCommandReceived.listen((command) { + appLogger.d('CompanionRemote: Command received: ${command.type}'); + + if (command.type == RemoteCommandType.deviceInfo) { + _handleDeviceInfo(command); + } else if (command.type == RemoteCommandType.ping || + command.type == RemoteCommandType.pong || + command.type == RemoteCommandType.ack) { + // Don't call callback for these + } else { + onCommandReceived?.call(command); + } + }, onError: (error) { + appLogger.e('CompanionRemote: Stream error', error: error); + }); + + _deviceConnectedSubscription = _peerService!.onDeviceConnected.listen((device) async { + appLogger.d('CompanionRemote: Device connected: ${device.name}'); + _session = _session?.copyWith( + status: RemoteSessionStatus.connected, + connectedDevice: device, + ); + notifyListeners(); + + await addTrustedDevice(device, requireApproval: isHost); + }); + + _deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) { + appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)'); + if (_intentionalDisconnect) { + _session = _session?.copyWith( + status: RemoteSessionStatus.disconnected, + connectedDevice: null, + ); + notifyListeners(); + } else { + _session = _session?.copyWith( + status: RemoteSessionStatus.reconnecting, + ); + notifyListeners(); + _scheduleReconnect(); + } + }); + + _errorSubscription = _peerService!.onError.listen((error) { + appLogger.e('CompanionRemote: Error: ${error.message}'); + _session = _session?.copyWith( + status: RemoteSessionStatus.error, + errorMessage: error.message, + ); + notifyListeners(); + }); + + _statusSubscription = _peerService!.onConnectionStateChanged.listen((status) { + appLogger.d('CompanionRemote: Status changed: $status'); + _session = _session?.copyWith(status: status); + notifyListeners(); + }); + } + + Future _handleDeviceInfo(RemoteCommand command) async { + if (command.data != null) { + 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'); + + final device = RemoteDevice( + id: command.deviceId, + name: command.deviceName, + platform: platform, + ); + + _session = _session?.copyWith(connectedDevice: device); + notifyListeners(); + + // Save to recent sessions now that we have the remote device's real identity + await _addToRecentSessions(); + } + } + + void _cleanupSubscriptions() { + _commandSubscription?.cancel(); + _commandSubscription = null; + _deviceConnectedSubscription?.cancel(); + _deviceConnectedSubscription = null; + _deviceDisconnectedSubscription?.cancel(); + _deviceDisconnectedSubscription = null; + _errorSubscription?.cancel(); + _errorSubscription = null; + _statusSubscription?.cancel(); + _statusSubscription = null; + } + + Future<({String sessionId, String pin})> createSession() async { + await leaveSession(); + + appLogger.d('CompanionRemote: Creating session as host'); + + _peerService = CompanionRemotePeerService(); + _setupPeerServiceListeners(); + + try { + final result = await _peerService!.createSession(_deviceName, _platform); + + _session = RemoteSession( + sessionId: result.sessionId, + pin: result.pin, + role: RemoteSessionRole.host, + status: RemoteSessionStatus.connected, + ); + + notifyListeners(); + appLogger.d('CompanionRemote: Session created - ID: ${result.sessionId}, PIN: ${result.pin}'); + + return result; + } catch (e) { + appLogger.e('CompanionRemote: Failed to create session', error: e); + _session = RemoteSession( + sessionId: '', + pin: '', + role: RemoteSessionRole.host, + status: RemoteSessionStatus.error, + errorMessage: e.toString(), + ); + notifyListeners(); + rethrow; + } + } + + Future joinSession(String sessionId, String pin) async { + await leaveSession(); + + _lastSessionId = sessionId; + _lastPin = pin; + + appLogger.d('CompanionRemote: Joining session - ID: $sessionId'); + + _peerService = CompanionRemotePeerService(); + _setupPeerServiceListeners(); + + _session = RemoteSession( + sessionId: sessionId, + pin: pin, + role: RemoteSessionRole.remote, + status: RemoteSessionStatus.connecting, + ); + notifyListeners(); + + try { + await _peerService!.joinSession(sessionId, pin, _deviceName, _platform); + + _session = _session?.copyWith(status: RemoteSessionStatus.connected); + notifyListeners(); + appLogger.d('CompanionRemote: Successfully joined session'); + } catch (e) { + appLogger.e('CompanionRemote: Failed to join session', error: e); + _session = _session?.copyWith( + status: RemoteSessionStatus.error, + errorMessage: e.toString(), + ); + notifyListeners(); + rethrow; + } + } + + void sendCommand(RemoteCommandType type, {Map? data}) { + if (_peerService == null || !isConnected) { + appLogger.w('CompanionRemote: Cannot send command - not connected'); + return; + } + + appLogger.d('CompanionRemote: Sending command $type'); + final command = RemoteCommand( + type: type, + deviceId: _peerService!.myPeerId ?? 'unknown', + deviceName: _deviceName, + data: data, + ); + + _peerService!.sendCommand(command); + } + + void _scheduleReconnect() { + if (_reconnectAttempts >= _maxReconnectAttempts) { + appLogger.w('CompanionRemote: Max reconnect attempts reached'); + _session = _session?.copyWith( + status: RemoteSessionStatus.error, + errorMessage: 'Connection lost after $_maxReconnectAttempts attempts', + ); + _reconnectAttempts = 0; + notifyListeners(); + return; + } + + final delay = Duration(seconds: 1 << _reconnectAttempts); // 1s, 2s, 4s, 8s, 16s + _reconnectAttempts++; + appLogger.d('CompanionRemote: Reconnect attempt $_reconnectAttempts in ${delay.inSeconds}s'); + + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(delay, _attemptReconnect); + } + + Future _attemptReconnect() async { + if (_lastSessionId == null || _lastPin == null) { + appLogger.w('CompanionRemote: No stored credentials for reconnect'); + _session = _session?.copyWith( + status: RemoteSessionStatus.error, + errorMessage: 'Connection lost', + ); + notifyListeners(); + return; + } + + try { + appLogger.d('CompanionRemote: Attempting reconnect...'); + // Clean up old peer service without triggering intentional disconnect + _cleanupSubscriptions(); + await _peerService?.disconnect(); + + _peerService = CompanionRemotePeerService(); + _setupPeerServiceListeners(); + + await _peerService!.joinSession(_lastSessionId!, _lastPin!, _deviceName, _platform); + + _session = _session?.copyWith(status: RemoteSessionStatus.connected); + _reconnectAttempts = 0; + notifyListeners(); + appLogger.d('CompanionRemote: Reconnected successfully'); + } catch (e) { + appLogger.e('CompanionRemote: Reconnect failed', error: e); + if (_session?.status == RemoteSessionStatus.reconnecting) { + _scheduleReconnect(); + } + } + } + + /// Immediately retry reconnection, skipping the backoff wait + void retryReconnectNow() { + _reconnectTimer?.cancel(); + _reconnectAttempts = 0; + _attemptReconnect(); + } + + /// Cancel ongoing reconnection attempts + void cancelReconnect() { + _reconnectTimer?.cancel(); + _reconnectAttempts = 0; + _session = _session?.copyWith( + status: RemoteSessionStatus.disconnected, + connectedDevice: null, + ); + notifyListeners(); + } + + Future leaveSession() async { + _intentionalDisconnect = true; + _reconnectTimer?.cancel(); + _reconnectAttempts = 0; + + if (_peerService != null) { + appLogger.d('CompanionRemote: Leaving session'); + await _peerService!.disconnect(); + _peerService = null; + } + + _cleanupSubscriptions(); + + _session = null; + _intentionalDisconnect = false; + notifyListeners(); + } + + Future _loadTrustedDevices() async { + try { + final storage = await StorageService.getInstance(); + final json = storage.prefs.getString(_storageKey); + if (json != null) { + final List list = jsonDecode(json); + _trustedDevices.clear(); + _trustedDevices.addAll( + list.map((e) => TrustedDevice.fromJson(e as Map)), + ); + appLogger.d('CompanionRemote: Loaded ${_trustedDevices.length} trusted devices'); + } + } catch (e) { + appLogger.e('CompanionRemote: Failed to load trusted devices', error: e); + } + } + + Future _saveTrustedDevices() async { + try { + final storage = await StorageService.getInstance(); + final json = jsonEncode(_trustedDevices.map((e) => e.toJson()).toList()); + await storage.prefs.setString(_storageKey, json); + appLogger.d('CompanionRemote: Saved ${_trustedDevices.length} trusted devices'); + } catch (e) { + appLogger.e('CompanionRemote: Failed to save trusted devices', error: e); + } + } + + bool isDeviceTrusted(String peerId) { + return _trustedDevices.any((d) => d.peerId == peerId && d.isApproved); + } + + Future addTrustedDevice(RemoteDevice device, {bool requireApproval = true}) async { + final existing = _trustedDevices.where((d) => d.peerId == device.id).firstOrNull; + + if (existing != null) { + final updated = existing.copyWith( + deviceName: device.name, + platform: device.platform, + lastConnected: DateTime.now(), + isApproved: !requireApproval || existing.isApproved, + ); + _trustedDevices.remove(existing); + _trustedDevices.add(updated); + } else { + bool approved = !requireApproval; + + if (requireApproval && onDeviceApprovalRequired != null) { + approved = await onDeviceApprovalRequired!(device); + } + + _trustedDevices.add( + TrustedDevice( + peerId: device.id, + deviceName: device.name, + platform: device.platform, + isApproved: approved, + ), + ); + } + + await _saveTrustedDevices(); + + if (isRemote) { + final storage = await StorageService.getInstance(); + await storage.prefs.setString(_lastDeviceKey, device.id); + } + + notifyListeners(); + } + + Future removeTrustedDevice(String peerId) async { + _trustedDevices.removeWhere((d) => d.peerId == peerId); + await _saveTrustedDevices(); + notifyListeners(); + } + + Future approveTrustedDevice(String peerId) async { + final device = _trustedDevices.where((d) => d.peerId == peerId).firstOrNull; + if (device != null) { + final updated = device.copyWith(isApproved: true); + _trustedDevices.remove(device); + _trustedDevices.add(updated); + await _saveTrustedDevices(); + notifyListeners(); + } + } + + Future getLastConnectedDevicePeerId() async { + final storage = await StorageService.getInstance(); + return storage.prefs.getString(_lastDeviceKey); + } + + /// Load recent sessions + Future loadRecentSessions() async { + try { + _discoveryService = CompanionRemoteDiscoveryService(); + + // Listen for recent sessions updates + _discoveryService!.recentSessions.listen((sessions) { + _recentSessions.clear(); + _recentSessions.addAll(sessions); + notifyListeners(); + }); + + // Initial load happens in constructor, just notify + _recentSessions.clear(); + _recentSessions.addAll(_discoveryService!.currentSessions); + notifyListeners(); + + appLogger.d('CompanionRemote: Loaded ${_recentSessions.length} recent sessions'); + } catch (e) { + appLogger.e('CompanionRemote: Failed to load recent sessions', error: e); + } + } + + /// Add current session to recent list (called after successful connection) + Future _addToRecentSessions() async { + if (_session == null || _session!.sessionId.isEmpty) return; + + // For mobile (remote role), save the connected desktop device + // For desktop (host role), this doesn't really apply but save connected mobile device + final deviceToSave = _session!.connectedDevice; + if (deviceToSave == null) { + appLogger.w('CompanionRemote: No connected device to save to recent sessions'); + return; + } + + final recentSession = RecentRemoteSession( + sessionId: _session!.sessionId, + pin: _session!.pin, + deviceName: deviceToSave.name, + platform: deviceToSave.platform, + lastConnected: DateTime.now(), + ); + + if (_discoveryService != null) { + await _discoveryService!.addRecentSession(recentSession); + } + } + + /// Connect to a recent session + Future connectToRecentSession(RecentRemoteSession session) async { + await joinSession(session.sessionId, session.pin); + } + + /// Remove a recent session + Future removeRecentSession(String sessionId) async { + if (_discoveryService != null) { + await _discoveryService!.removeRecentSession(sessionId); + } + } + + /// Clear all recent sessions + Future clearRecentSessions() async { + if (_discoveryService != null) { + await _discoveryService!.clearRecentSessions(); + } + } + + @override + void dispose() { + _reconnectTimer?.cancel(); + leaveSession(); + _discoveryService?.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/companion_remote/mobile_remote_screen.dart b/lib/screens/companion_remote/mobile_remote_screen.dart new file mode 100644 index 00000000..7afe049a --- /dev/null +++ b/lib/screens/companion_remote/mobile_remote_screen.dart @@ -0,0 +1,731 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import '../../models/companion_remote/remote_command_type.dart'; +import '../../models/companion_remote/remote_session.dart'; +import '../../providers/companion_remote_provider.dart'; +import '../../utils/platform_detector.dart'; +import '../../utils/app_logger.dart'; +import 'pairing_screen.dart'; + +class MobileRemoteScreen extends StatefulWidget { + const MobileRemoteScreen({super.key}); + + @override + State createState() => _MobileRemoteScreenState(); +} + +class _MobileRemoteScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Companion Remote'), + actions: [ + Consumer( + builder: (context, provider, child) { + if (provider.isConnected) { + return IconButton( + icon: const Icon(Icons.link_off), + onPressed: () async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Disconnect'), + content: const Text('Do you want to disconnect from the remote session?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Disconnect'), + ), + ], + ), + ); + + if (confirmed == true && context.mounted) { + await context.read().leaveSession(); + } + }, + tooltip: 'Disconnect', + ); + } + return const SizedBox.shrink(); + }, + ), + ], + ), + body: Consumer( + builder: (context, provider, child) { + if (provider.status == RemoteSessionStatus.reconnecting) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 24), + Text( + 'Reconnecting...', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + 'Attempt ${provider.reconnectAttempts} of 5', + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton( + onPressed: () => provider.cancelReconnect(), + child: const Text('Cancel'), + ), + const SizedBox(width: 16), + FilledButton( + onPressed: () => provider.retryReconnectNow(), + child: const Text('Retry Now'), + ), + ], + ), + ], + ), + ); + } + + if (!provider.isConnected) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.phonelink_off, size: 64, color: Colors.grey), + const SizedBox(height: 16), + Text( + provider.status == RemoteSessionStatus.error + ? provider.session?.errorMessage ?? 'Connection error' + : 'Not connected', + style: const TextStyle(fontSize: 20, color: Colors.grey), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + FilledButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const PairingScreen()), + ); + }, + icon: const Icon(Icons.link), + label: const Text('Connect to Device'), + ), + ], + ), + ); + } + + return const _RemoteControlLayout(); + }, + ), + ); + } +} + +class _RemoteControlLayout extends StatelessWidget { + const _RemoteControlLayout(); + + @override + Widget build(BuildContext context) { + if (PlatformDetector.isDesktop(context)) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: const _RemoteControlContent(), + ), + ); + } + + return const _RemoteControlContent(); + } +} + +class _RemoteControlContent extends StatefulWidget { + const _RemoteControlContent(); + + @override + State<_RemoteControlContent> createState() => _RemoteControlContentState(); +} + +class _RemoteControlContentState extends State<_RemoteControlContent> { + int _selectedTab = 0; + + void _showSearchSheet({bool switchToSearchTab = false}) { + if (switchToSearchTab) { + _sendCommand(RemoteCommandType.tabSearch); + } + final provider = context.read(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => _SearchBottomSheet(provider: provider), + ); + } + + void _sendCommand(RemoteCommandType type) { + HapticFeedback.lightImpact(); + print('🔴 MobileRemoteScreen: Button pressed! Type: $type'); + appLogger.d('MobileRemoteScreen: Sending command: $type'); + final provider = context.read(); + print('🔴 Provider isConnected: ${provider.isConnected}'); + provider.sendCommand(type); + print('🔴 sendCommand called'); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Consumer( + builder: (context, provider, child) { + final device = provider.connectedDevice; + if (device == null) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.all(16), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + children: [ + Icon( + Icons.computer, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + device.name, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + Text( + device.platform, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ], + ), + ), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + ], + ), + ); + }, + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + SegmentedButton( + segments: const [ + ButtonSegment(value: 0, label: Text('Navigate'), icon: Icon(Icons.navigation)), + ButtonSegment(value: 1, label: Text('Playback'), icon: Icon(Icons.play_arrow)), + ButtonSegment(value: 2, label: Text('Quick'), icon: Icon(Icons.flash_on)), + ], + selected: {_selectedTab}, + onSelectionChanged: (Set selection) { + setState(() { + _selectedTab = selection.first; + }); + }, + ), + const SizedBox(height: 24), + if (_selectedTab == 0) _buildNavigationTab(), + if (_selectedTab == 1) _buildPlaybackTab(), + if (_selectedTab == 2) _buildQuickActionsTab(), + ], + ), + ), + ), + ], + ); + } + + Widget _buildNavigationTab() { + return Column( + children: [ + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _RemoteButton( + icon: Icons.home, + label: 'Home', + onPressed: () => _sendCommand(RemoteCommandType.home), + ), + _RemoteButton( + icon: Icons.arrow_back, + label: 'Back', + onPressed: () => _sendCommand(RemoteCommandType.back), + ), + _RemoteButton( + icon: Icons.menu, + label: 'Menu', + onPressed: () => _sendCommand(RemoteCommandType.contextMenu), + ), + ], + ), + const SizedBox(height: 32), + Center( + child: _DPad(onCommand: _sendCommand), + ), + const SizedBox(height: 32), + Text( + 'Tab Navigation', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.center, + children: [ + _RemoteChip( + icon: Icons.explore, + label: 'Discover', + onPressed: () => _sendCommand(RemoteCommandType.tabDiscover), + ), + _RemoteChip( + icon: Icons.video_library, + label: 'Libraries', + onPressed: () => _sendCommand(RemoteCommandType.tabLibraries), + ), + _RemoteChip( + icon: Icons.search, + label: 'Search', + onPressed: () => _showSearchSheet(switchToSearchTab: true), + ), + _RemoteChip( + icon: Icons.download, + label: 'Downloads', + onPressed: () => _sendCommand(RemoteCommandType.tabDownloads), + ), + _RemoteChip( + icon: Icons.settings, + label: 'Settings', + onPressed: () => _sendCommand(RemoteCommandType.tabSettings), + ), + ], + ), + ], + ); + } + + Widget _buildPlaybackTab() { + return Column( + children: [ + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _RemoteButton( + icon: Icons.skip_previous, + label: 'Previous', + onPressed: () => _sendCommand(RemoteCommandType.previousTrack), + ), + const SizedBox(width: 16), + _RemoteButton( + icon: Icons.play_arrow, + label: 'Play/Pause', + size: 64, + iconSize: 36, + onPressed: () => _sendCommand(RemoteCommandType.playPause), + ), + const SizedBox(width: 16), + _RemoteButton( + icon: Icons.skip_next, + label: 'Next', + onPressed: () => _sendCommand(RemoteCommandType.nextTrack), + ), + ], + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _RemoteButton( + icon: Icons.replay_10, + label: 'Seek Back', + onPressed: () => _sendCommand(RemoteCommandType.seekBackward), + ), + const SizedBox(width: 16), + _RemoteButton( + icon: Icons.stop, + label: 'Stop', + onPressed: () => _sendCommand(RemoteCommandType.stop), + ), + const SizedBox(width: 16), + _RemoteButton( + icon: Icons.forward_10, + label: 'Seek Fwd', + onPressed: () => _sendCommand(RemoteCommandType.seekForward), + ), + ], + ), + const SizedBox(height: 32), + Text( + 'Volume', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _RemoteButton( + icon: Icons.volume_off, + label: 'Mute', + onPressed: () => _sendCommand(RemoteCommandType.volumeMute), + ), + const SizedBox(width: 16), + _RemoteButton( + icon: Icons.volume_down, + label: 'Down', + onPressed: () => _sendCommand(RemoteCommandType.volumeDown), + ), + const SizedBox(width: 16), + _RemoteButton( + icon: Icons.volume_up, + label: 'Up', + onPressed: () => _sendCommand(RemoteCommandType.volumeUp), + ), + ], + ), + ], + ); + } + + Widget _buildQuickActionsTab() { + return Column( + children: [ + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + alignment: WrapAlignment.center, + children: [ + _RemoteCard( + icon: Icons.search, + label: 'Search', + onPressed: _showSearchSheet, + ), + _RemoteCard( + icon: Icons.fullscreen, + label: 'Fullscreen', + onPressed: () => _sendCommand(RemoteCommandType.fullscreen), + ), + ], + ), + ], + ); + } +} + +class _DPad extends StatelessWidget { + final Function(RemoteCommandType) onCommand; + + const _DPad({required this.onCommand}); + + @override + Widget build(BuildContext context) { + const size = 80.0; + const centerSize = 60.0; + + return SizedBox( + width: size * 3, + height: size * 3, + child: Stack( + children: [ + Positioned( + left: size, + top: 0, + child: _DPadButton( + icon: Icons.arrow_drop_up, + onPressed: () => onCommand(RemoteCommandType.dpadUp), + size: size, + ), + ), + Positioned( + left: size, + bottom: 0, + child: _DPadButton( + icon: Icons.arrow_drop_down, + onPressed: () => onCommand(RemoteCommandType.dpadDown), + size: size, + ), + ), + Positioned( + left: 0, + top: size, + child: _DPadButton( + icon: Icons.arrow_left, + onPressed: () => onCommand(RemoteCommandType.dpadLeft), + size: size, + ), + ), + Positioned( + right: 0, + top: size, + child: _DPadButton( + icon: Icons.arrow_right, + onPressed: () => onCommand(RemoteCommandType.dpadRight), + size: size, + ), + ), + Positioned( + left: (size * 3 - centerSize) / 2, + top: (size * 3 - centerSize) / 2, + child: _DPadButton( + icon: Icons.check, + label: 'OK', + onPressed: () => onCommand(RemoteCommandType.select), + size: centerSize, + isPrimary: true, + ), + ), + ], + ), + ); + } +} + +class _DPadButton extends StatelessWidget { + final IconData icon; + final String? label; + final VoidCallback onPressed; + final double size; + final bool isPrimary; + + const _DPadButton({ + required this.icon, + this.label, + required this.onPressed, + required this.size, + this.isPrimary = false, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: size, + height: size, + child: ElevatedButton( + onPressed: () { + HapticFeedback.lightImpact(); + onPressed(); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + shape: const CircleBorder(), + backgroundColor: isPrimary ? Theme.of(context).colorScheme.primary : null, + foregroundColor: isPrimary ? Theme.of(context).colorScheme.onPrimary : null, + ), + child: label != null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 24), + const SizedBox(height: 2), + Text(label!, style: const TextStyle(fontSize: 10)), + ], + ) + : Icon(icon, size: 36), + ), + ); + } +} + +class _RemoteButton extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onPressed; + final double size; + final double iconSize; + + const _RemoteButton({ + required this.icon, + required this.label, + required this.onPressed, + this.size = 56, + this.iconSize = 24, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: size, + height: size, + child: FilledButton( + onPressed: () { + HapticFeedback.lightImpact(); + onPressed(); + }, + style: FilledButton.styleFrom( + padding: EdgeInsets.zero, + shape: const CircleBorder(), + ), + child: Icon(icon, size: iconSize), + ), + ), + const SizedBox(height: 4), + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ); + } +} + +class _RemoteChip extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onPressed; + + const _RemoteChip({ + required this.icon, + required this.label, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return ActionChip( + avatar: Icon(icon, size: 18), + label: Text(label), + onPressed: () { + HapticFeedback.lightImpact(); + onPressed(); + }, + ); + } +} + +class _SearchBottomSheet extends StatefulWidget { + final CompanionRemoteProvider provider; + + const _SearchBottomSheet({required this.provider}); + + @override + State<_SearchBottomSheet> createState() => _SearchBottomSheetState(); +} + +class _SearchBottomSheetState extends State<_SearchBottomSheet> { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _submit(String text) { + final trimmed = text.trim(); + if (trimmed.isNotEmpty) { + widget.provider.sendCommand(RemoteCommandType.search, data: {'query': trimmed}); + } + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + left: 16, + right: 16, + top: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _controller, + autofocus: true, + decoration: InputDecoration( + hintText: 'Search on desktop...', + prefixIcon: const Icon(Icons.search), + suffixIcon: IconButton( + icon: const Icon(Icons.send), + onPressed: () => _submit(_controller.text), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + ), + ), + onSubmitted: _submit, + ), + const SizedBox(height: 16), + ], + ), + ); + } +} + +class _RemoteCard extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onPressed; + + const _RemoteCard({ + required this.icon, + required this.label, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 100, + height: 100, + child: Card( + child: InkWell( + onTap: () { + HapticFeedback.lightImpact(); + onPressed(); + }, + borderRadius: BorderRadius.circular(12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 32), + const SizedBox(height: 8), + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/companion_remote/pairing_screen.dart b/lib/screens/companion_remote/pairing_screen.dart new file mode 100644 index 00000000..ca4d92ae --- /dev/null +++ b/lib/screens/companion_remote/pairing_screen.dart @@ -0,0 +1,653 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:provider/provider.dart'; + +import '../../providers/companion_remote_provider.dart'; +import '../../services/companion_remote/companion_remote_discovery_service.dart'; +import '../../utils/app_logger.dart'; + +class PairingScreen extends StatefulWidget { + const PairingScreen({super.key}); + + @override + State createState() => _PairingScreenState(); +} + +class _PairingScreenState extends State { + final _sessionIdController = TextEditingController(); + final _pinController = TextEditingController(); + final _formKey = GlobalKey(); + bool _isConnecting = false; + String? _connectingSessionId; + bool _isDiscovering = false; + String? _errorMessage; + int _selectedTab = 0; + + // QR scanner state + MobileScannerController? _scannerController; + String? _lastScannedCode; + + bool get _isMobile => Platform.isAndroid || Platform.isIOS; + + // Tab indices shift when scan tab is present + int get _scanTabIndex => _isMobile ? 1 : -1; + int get _manualTabIndex => _isMobile ? 2 : 1; + + @override + void initState() { + super.initState(); + _loadRecentSessions(); + } + + @override + void dispose() { + _sessionIdController.dispose(); + _pinController.dispose(); + _scannerController?.dispose(); + super.dispose(); + } + + Future _loadRecentSessions() async { + setState(() { + _isDiscovering = true; + _errorMessage = null; + }); + + try { + await context.read().loadRecentSessions(); + setState(() { + _isDiscovering = false; + }); + } catch (e) { + appLogger.e('Failed to load recent sessions', error: e); + setState(() { + _isDiscovering = false; + _errorMessage = 'Failed to load recent sessions: ${e.toString()}'; + }); + } + } + + Future _connectToRecentSession(RecentRemoteSession session) async { + setState(() { + _isConnecting = true; + _connectingSessionId = session.sessionId; + _errorMessage = null; + }); + + try { + await context.read().connectToRecentSession(session); + + if (mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + appLogger.e('Failed to connect to recent session', error: e); + setState(() { + _isConnecting = false; + _connectingSessionId = null; + _errorMessage = _parseErrorMessage(e.toString()); + }); + } + } + + Future _connect() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isConnecting = true; + _errorMessage = null; + }); + + try { + final provider = context.read(); + await provider.joinSession( + _sessionIdController.text.trim().toUpperCase(), + _pinController.text.trim(), + ); + + if (mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + appLogger.e('Failed to join remote session', error: e); + setState(() { + _isConnecting = false; + _errorMessage = _parseErrorMessage(e.toString()); + }); + } + } + + String _parseErrorMessage(String error) { + if (error.contains('timeout') || error.contains('Timed out')) { + return 'Connection timed out. Please check the session ID and PIN.'; + } else if (error.contains('Failed to connect')) { + return 'Could not find the session. Please check your credentials.'; + } + return 'Failed to connect: ${error.replaceAll('Exception: ', '')}'; + } + + void _handleQrCode(String data) { + // Debounce: don't process the same code twice + if (data == _lastScannedCode) return; + _lastScannedCode = data; + + final parts = data.split(':'); + if (parts.length == 2) { + _scannerController?.stop(); + setState(() { + _errorMessage = null; + _isConnecting = true; + }); + // Connect directly instead of going through _connect() which requires Form validation + _connectWithCredentials(parts[0], parts[1]); + } else { + setState(() { + _errorMessage = 'Invalid QR code format'; + }); + } + } + + Future _connectWithCredentials(String sessionId, String pin) async { + try { + final provider = context.read(); + await provider.joinSession(sessionId.trim().toUpperCase(), pin.trim()); + + if (mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + appLogger.e('Failed to join remote session', error: e); + _lastScannedCode = null; // Allow re-scanning + setState(() { + _isConnecting = false; + _errorMessage = _parseErrorMessage(e.toString()); + }); + _scannerController?.start(); + } + } + + Future _pasteFromClipboard(TextEditingController controller) async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null) { + setState(() { + controller.text = data!.text!; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Connect to Device'), + actions: [ + if (_selectedTab == 0) + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _isDiscovering ? null : _loadRecentSessions, + tooltip: 'Refresh', + ), + ], + ), + body: Column( + children: [ + SegmentedButton( + segments: [ + const ButtonSegment( + value: 0, + label: Text('Recent'), + icon: Icon(Icons.history), + ), + if (_isMobile) + ButtonSegment( + value: _scanTabIndex, + label: const Text('Scan'), + icon: const Icon(Icons.qr_code_scanner), + ), + ButtonSegment( + value: _manualTabIndex, + label: const Text('Manual'), + icon: const Icon(Icons.keyboard), + ), + ], + selected: {_selectedTab}, + onSelectionChanged: (Set selection) { + setState(() { + _selectedTab = selection.first; + }); + }, + ), + Expanded( + child: _buildTabContent(), + ), + ], + ), + ); + } + + Widget _buildTabContent() { + if (_selectedTab == 0) return _buildDiscoveryTab(); + if (_selectedTab == _scanTabIndex) return _buildScanTab(); + return _buildManualEntryTab(); + } + + Widget _buildScanTab() { + return Column( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: MobileScanner( + controller: _scannerController ??= MobileScannerController(), + onDetect: (capture) { + final barcode = capture.barcodes.firstOrNull; + if (barcode?.rawValue != null) { + _handleQrCode(barcode!.rawValue!); + } + }, + errorBuilder: (context, error, child) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.no_photography, size: 64, color: Colors.grey), + const SizedBox(height: 16), + Text( + error.errorCode == MobileScannerErrorCode.permissionDenied + ? 'Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.' + : 'Could not start camera: ${error.errorDetails?.message ?? error.errorCode.name}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ); + }, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + children: [ + Text( + 'Point your camera at the QR code shown on your desktop', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + if (_errorMessage != null) ...[ + const SizedBox(height: 12), + Card( + color: Theme.of(context).colorScheme.errorContainer, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Icon(Icons.error_outline, color: Theme.of(context).colorScheme.onErrorContainer), + const SizedBox(width: 12), + Expanded( + child: Text( + _errorMessage!, + style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer), + ), + ), + ], + ), + ), + ), + ], + ], + ), + ), + ], + ); + } + + Widget _buildDiscoveryTab() { + return Consumer( + builder: (context, provider, child) { + final sessions = provider.recentSessions; + + return SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon(Icons.history, size: 64, color: Colors.blue), + const SizedBox(height: 24), + Text( + 'Recent Connections', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Quickly reconnect to previously paired devices', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + if (_isDiscovering) ...[ + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 16), + Text( + 'Loading...', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ] else if (sessions.isEmpty) ...[ + Card( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + Icon( + Icons.devices_other, + size: 48, + color: Theme.of(context).colorScheme.outline, + ), + const SizedBox(height: 16), + Text( + 'No recent connections', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + 'Connect to a device using Manual entry to get started', + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ] else ...[ + ...sessions.map((session) { + final isThisConnecting = _isConnecting && _connectingSessionId == session.sessionId; + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: const Icon(Icons.computer, size: 40), + title: Text(session.deviceName), + subtitle: Text( + '${session.platform}\n' + 'Session: ${session.sessionId}\n' + 'Last used: ${_formatDate(session.lastConnected)}', + ), + isThreeLine: true, + trailing: isThisConnecting + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.arrow_forward), + onTap: _isConnecting ? null : () => _connectToRecentSession(session), + onLongPress: () => _showRemoveSessionDialog(session), + ), + ); + }), + ], + if (_errorMessage != null) ...[ + const SizedBox(height: 16), + Card( + color: Theme.of(context).colorScheme.errorContainer, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _errorMessage!, + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + ), + ], + ], + ), + ); + }, + ); + } + + String _formatDate(DateTime date) { + final now = DateTime.now(); + final difference = now.difference(date); + + if (difference.inMinutes < 1) { + return 'Just now'; + } else if (difference.inHours < 1) { + return '${difference.inMinutes}m ago'; + } else if (difference.inDays < 1) { + return '${difference.inHours}h ago'; + } else if (difference.inDays < 7) { + return '${difference.inDays}d ago'; + } else { + return '${date.month}/${date.day}/${date.year}'; + } + } + + Future _showRemoveSessionDialog(RecentRemoteSession session) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Remove Recent Connection'), + content: Text('Remove "${session.deviceName}" from recent connections?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Remove'), + ), + ], + ), + ); + + if (confirmed == true && mounted) { + await context.read().removeRecentSession(session.sessionId); + } + } + + Widget _buildManualEntryTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon(Icons.keyboard, size: 64, color: Colors.blue), + const SizedBox(height: 24), + Text( + 'Pair with Desktop', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Enter the session details shown on your desktop device', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + if (_errorMessage != null) ...[ + Card( + color: Theme.of(context).colorScheme.errorContainer, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _errorMessage!, + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + ], + TextFormField( + controller: _sessionIdController, + decoration: InputDecoration( + labelText: 'Session ID', + hintText: 'Enter 8-character session ID', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.vpn_key), + suffixIcon: IconButton( + icon: const Icon(Icons.paste), + onPressed: () => _pasteFromClipboard(_sessionIdController), + tooltip: 'Paste', + ), + ), + textCapitalization: TextCapitalization.characters, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')), + LengthLimitingTextInputFormatter(8), + TextInputFormatter.withFunction((oldValue, newValue) { + return newValue.copyWith(text: newValue.text.toUpperCase()); + }), + ], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a session ID'; + } + if (value.length != 8) { + return 'Session ID must be 8 characters'; + } + return null; + }, + enabled: !_isConnecting, + ), + const SizedBox(height: 16), + TextFormField( + controller: _pinController, + decoration: InputDecoration( + labelText: 'PIN', + hintText: 'Enter 6-digit PIN', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.lock), + suffixIcon: IconButton( + icon: const Icon(Icons.paste), + onPressed: () => _pasteFromClipboard(_pinController), + tooltip: 'Paste', + ), + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(6), + ], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a PIN'; + } + if (value.length != 6) { + return 'PIN must be 6 digits'; + } + return null; + }, + enabled: !_isConnecting, + ), + const SizedBox(height: 24), + FilledButton.icon( + onPressed: _isConnecting ? null : _connect, + icon: _isConnecting + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.link), + label: Text(_isConnecting ? 'Connecting...' : 'Connect'), + ), + const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 16), + Text( + 'Tips', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + _buildTipCard( + context, + Icons.computer, + 'Open Plezy on your desktop and enable Companion Remote from settings or menu', + ), + if (_isMobile) ...[ + const SizedBox(height: 8), + _buildTipCard( + context, + Icons.qr_code, + 'Use the Scan tab to quickly pair by scanning the QR code on your desktop', + ), + ], + const SizedBox(height: 8), + _buildTipCard( + context, + Icons.wifi, + 'Make sure both devices are on the same WiFi network', + ), + ], + ), + ), + ); + } + + Widget _buildTipCard(BuildContext context, IconData icon, String text) { + return Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index c2bcb646..c2c326fc 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -39,6 +39,9 @@ import 'auth_screen.dart'; import 'libraries/state_messages.dart'; import 'main_screen.dart'; import '../watch_together/watch_together.dart'; +import '../providers/companion_remote_provider.dart'; +import '../widgets/companion_remote/remote_session_dialog.dart'; +import 'companion_remote/mobile_remote_screen.dart'; class DiscoverScreen extends StatefulWidget { final VoidCallback? onBecameVisible; @@ -118,6 +121,9 @@ class _DiscoverScreenState extends State _refreshContinueWatching(); } + // Track initial load so we can focus hero when content first appears + bool _initialLoadComplete = false; + // Hub navigation keys GlobalKey? _continueWatchingHubKey; final List> _hubKeys = []; @@ -126,9 +132,11 @@ class _DiscoverScreenState extends State late FocusNode _heroFocusNode; late FocusNode _refreshButtonFocusNode; late FocusNode _watchTogetherButtonFocusNode; + late FocusNode _companionRemoteButtonFocusNode; late FocusNode _userButtonFocusNode; bool _isRefreshFocused = false; bool _isWatchTogetherFocused = false; + bool _isCompanionRemoteFocused = false; bool _isUserFocused = false; /// Get the correct PlexClient for an item's server @@ -242,9 +250,11 @@ class _DiscoverScreenState extends State _heroFocusNode = FocusNode(debugLabel: 'hero_section'); _refreshButtonFocusNode = FocusNode(debugLabel: 'refresh_button'); _watchTogetherButtonFocusNode = FocusNode(debugLabel: 'watch_together_button'); + _companionRemoteButtonFocusNode = FocusNode(debugLabel: 'companion_remote_button'); _userButtonFocusNode = FocusNode(debugLabel: 'user_button'); _refreshButtonFocusNode.addListener(_onRefreshFocusChange); _watchTogetherButtonFocusNode.addListener(_onWatchTogetherFocusChange); + _companionRemoteButtonFocusNode.addListener(_onCompanionRemoteFocusChange); _userButtonFocusNode.addListener(_onUserFocusChange); _loadContent(); _startAutoScroll(); @@ -266,6 +276,14 @@ class _DiscoverScreenState extends State } } + void _onCompanionRemoteFocusChange() { + if (mounted) { + setState(() { + _isCompanionRemoteFocused = _companionRemoteButtonFocusNode.hasFocus; + }); + } + } + void _onUserFocusChange() { if (mounted) { setState(() { @@ -386,7 +404,13 @@ class _DiscoverScreenState extends State return KeyEventResult.handled; } - // RIGHT: Move to user button + // RIGHT: Move to companion remote button (desktop only) + if (key.isRightKey && PlatformDetector.isDesktop(context)) { + _companionRemoteButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + + // RIGHT: Move to user button (non-desktop, skip companion remote) if (key.isRightKey) { _userButtonFocusNode.requestFocus(); return KeyEventResult.handled; @@ -406,6 +430,46 @@ class _DiscoverScreenState extends State return KeyEventResult.ignored; } + /// Handle key events for the companion remote button in app bar + KeyEventResult _handleCompanionRemoteKeyEvent(FocusNode node, KeyEvent event) { + if (!event.isActionable) { + return KeyEventResult.ignored; + } + + final key = event.logicalKey; + + // DOWN: Return to hero + if (key.isDownKey) { + _heroFocusNode.requestFocus(); + return KeyEventResult.handled; + } + + // LEFT: Move to watch together button + if (key.isLeftKey) { + _watchTogetherButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + + // RIGHT: Move to user button + if (key.isRightKey) { + _userButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + + // UP: Block at boundary + if (key.isUpKey) { + return KeyEventResult.handled; + } + + // SELECT: Show companion remote dialog + if (key.isSelectKey) { + RemoteSessionDialog.show(context); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + } + /// Handle key events for the user button in app bar KeyEventResult _handleUserKeyEvent(FocusNode node, KeyEvent event) { if (!event.isActionable) { @@ -420,7 +484,13 @@ class _DiscoverScreenState extends State return KeyEventResult.handled; } - // LEFT: Move to watch together button + // LEFT: Move to companion remote button (desktop only) + if (key.isLeftKey && PlatformDetector.isDesktop(context)) { + _companionRemoteButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + + // LEFT: Move to watch together button (non-desktop, skip companion remote) if (key.isLeftKey) { _watchTogetherButtonFocusNode.requestFocus(); return KeyEventResult.handled; @@ -454,6 +524,8 @@ class _DiscoverScreenState extends State _refreshButtonFocusNode.dispose(); _watchTogetherButtonFocusNode.removeListener(_onWatchTogetherFocusChange); _watchTogetherButtonFocusNode.dispose(); + _companionRemoteButtonFocusNode.removeListener(_onCompanionRemoteFocusChange); + _companionRemoteButtonFocusNode.dispose(); _userButtonFocusNode.removeListener(_onUserFocusChange); _userButtonFocusNode.dispose(); super.dispose(); @@ -645,6 +717,16 @@ class _DiscoverScreenState extends State _heroController.jumpToPage(0); } + // On initial load, focus the hero so the user starts on content (not the toolbar) + if (!_initialLoadComplete && onDeck.isNotEmpty) { + _initialLoadComplete = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _heroFocusNode.canRequestFocus) { + _heroFocusNode.requestFocus(); + } + }); + } + // Wait for global hubs final allHubs = await hubsFuture; @@ -1037,6 +1119,60 @@ class _DiscoverScreenState extends State ); }, ), + // Companion Remote button + Consumer( + builder: (context, companionRemote, child) { + final isDesktop = PlatformDetector.isDesktop(context); + + return Focus( + focusNode: isDesktop ? _companionRemoteButtonFocusNode : null, + onKeyEvent: isDesktop ? _handleCompanionRemoteKeyEvent : null, + child: Container( + decoration: BoxDecoration( + color: isDesktop && _isCompanionRemoteFocused ? Colors.white.withValues(alpha: 0.2) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Stack( + children: [ + IconButton( + icon: AppIcon( + Symbols.phone_android_rounded, + fill: companionRemote.isConnected ? 1 : 0, + color: companionRemote.isConnected ? Theme.of(context).colorScheme.primary : Colors.white, + ), + onPressed: () { + if (isDesktop) { + RemoteSessionDialog.show(context); + } else { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => MobileRemoteScreen()), + ); + } + }, + tooltip: 'Companion Remote', + ), + // Badge showing connection status + if (companionRemote.isConnected) + Positioned( + top: 6, + right: 6, + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1), + ), + ), + ), + ], + ), + ), + ); + }, + ), Consumer( builder: (context, userProvider, child) { return Focus( @@ -1233,8 +1369,15 @@ class _DiscoverScreenState extends State ], ), ), - // Overlaid app bar - Positioned(top: 0, left: 0, right: 0, child: _buildOverlaidAppBar()), + // Overlaid app bar — excluded from default focus traversal so that + // initial/tab-switch focus lands on content (hero/hubs), not the toolbar. + // Toolbar buttons are still reachable via explicit UP from hero section. + Positioned( + top: 0, + left: 0, + right: 0, + child: ExcludeFocusTraversal(child: _buildOverlaidAppBar()), + ), ], ), ); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index c64e6dd3..dc4d0103 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -28,6 +28,8 @@ import '../services/settings_service.dart'; import '../providers/offline_mode_provider.dart'; import '../services/plex_auth_service.dart'; import '../services/storage_service.dart'; +import '../services/companion_remote/companion_remote_receiver.dart'; +import '../providers/companion_remote_provider.dart'; import '../utils/desktop_window_padding.dart'; import '../widgets/side_navigation_rail.dart'; import '../focus/key_event_utils.dart'; @@ -362,6 +364,8 @@ class _MainScreenState extends State with RouteAware, WindowListener } } + bool _companionRemoteSetup = false; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -378,9 +382,71 @@ class _MainScreenState extends State with RouteAware, WindowListener _offlineModeProvider!.addListener(_handleOfflineStatusChanged); } + // Wire up Companion Remote command routing (desktop only, once) + if (!_companionRemoteSetup && PlatformDetector.isDesktop(context)) { + _companionRemoteSetup = true; + _setupCompanionRemote(); + } + routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute); } + void _setupCompanionRemote() { + final companionRemote = context.read(); + companionRemote.onCommandReceived = (command) { + if (mounted) { + CompanionRemoteReceiver.instance.handleCommand(command, context); + } + }; + + final receiver = CompanionRemoteReceiver.instance; + final tabCount = _getVisibleTabs(_isOffline).length; + + receiver.onTabNext = () { + _selectTab((_currentIndex + 1) % tabCount); + }; + receiver.onTabPrevious = () { + _selectTab((_currentIndex - 1 + tabCount) % tabCount); + }; + receiver.onTabDiscover = () { + final idx = NavigationTab.indexFor(NavigationTabId.discover, isOffline: _isOffline); + if (idx >= 0) _selectTab(idx); + }; + receiver.onTabLibraries = () { + final idx = NavigationTab.indexFor(NavigationTabId.libraries, isOffline: _isOffline); + if (idx >= 0) _selectTab(idx); + }; + receiver.onTabSearch = () { + final idx = NavigationTab.indexFor(NavigationTabId.search, isOffline: _isOffline); + if (idx >= 0) _selectTab(idx); + }; + receiver.onTabDownloads = () { + final idx = NavigationTab.indexFor(NavigationTabId.downloads, isOffline: _isOffline); + if (idx >= 0) _selectTab(idx); + }; + receiver.onTabSettings = () { + final idx = NavigationTab.indexFor(NavigationTabId.settings, isOffline: _isOffline); + if (idx >= 0) _selectTab(idx); + }; + receiver.onHome = () { + final idx = NavigationTab.indexFor(NavigationTabId.discover, isOffline: _isOffline); + if (idx >= 0) _selectTab(idx); + }; + receiver.onSearchAction = (query) { + final idx = NavigationTab.indexFor(NavigationTabId.search, isOffline: _isOffline); + if (idx >= 0) { + _selectTab(idx); + if (query != null && query.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_searchKey.currentState case final SearchInputFocusable searchable) { + searchable.setSearchQuery(query); + } + }); + } + } + }; + } + @override void dispose() { WidgetsBinding.instance.removeObserver(this); @@ -392,6 +458,21 @@ class _MainScreenState extends State with RouteAware, WindowListener _offlineModeProvider?.removeListener(_handleOfflineStatusChanged); _sidebarFocusScope.dispose(); _contentFocusScope.dispose(); + + // Clean up companion remote callbacks + if (_companionRemoteSetup) { + final receiver = CompanionRemoteReceiver.instance; + receiver.onTabNext = null; + receiver.onTabPrevious = null; + receiver.onTabDiscover = null; + receiver.onTabLibraries = null; + receiver.onTabSearch = null; + receiver.onTabDownloads = null; + receiver.onTabSettings = null; + receiver.onHome = null; + receiver.onSearchAction = null; + } + super.dispose(); } diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 86e0eff1..b404ab00 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -133,6 +133,12 @@ class _SearchScreenState extends State with Refreshable, FullRefre FocusUtils.requestFocusAfterBuild(this, _searchFocusNode); } + /// Set the search query externally (e.g. from companion remote) + @override + void setSearchQuery(String query) { + _searchController.text = query; + } + // Public method to fully reload all content (for profile switches) @override void fullRefresh() { diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index a7ae977f..14f4de84 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -27,6 +27,9 @@ import '../../utils/platform_detector.dart'; import '../../widgets/desktop_app_bar.dart'; import '../../widgets/tv_number_spinner.dart'; import 'hotkey_recorder_widget.dart'; +import '../../providers/companion_remote_provider.dart'; +import '../../screens/companion_remote/mobile_remote_screen.dart'; +import '../../widgets/companion_remote/remote_session_dialog.dart'; import 'about_screen.dart'; import 'logs_screen.dart'; import 'mpv_config_screen.dart'; @@ -206,6 +209,8 @@ class _SettingsScreenState extends State with FocusableTab { _buildDownloadsSection(), const SizedBox(height: 24), if (_keyboardShortcutsSupported) ...[_buildKeyboardShortcutsSection(), const SizedBox(height: 24)], + _buildCompanionRemoteSection(), + const SizedBox(height: 24), _buildAdvancedSection(), const SizedBox(height: 24), if (UpdateService.isUpdateCheckEnabled) ...[_buildUpdateSection(), const SizedBox(height: 24)], @@ -776,6 +781,54 @@ class _SettingsScreenState extends State with FocusableTab { ); } + Widget _buildCompanionRemoteSection() { + return Consumer( + builder: (context, companionRemote, child) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Companion Remote', + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + if (PlatformDetector.isDesktop(context)) + ListTile( + leading: const AppIcon(Symbols.phone_android_rounded, fill: 1), + title: const Text('Host Remote Session'), + subtitle: companionRemote.isConnected + ? Text('Connected to ${companionRemote.connectedDevice?.name}') + : const Text('Control this device with your phone'), + trailing: companionRemote.isConnected + ? const AppIcon(Symbols.check_circle_rounded, fill: 1, color: Colors.green) + : const AppIcon(Symbols.chevron_right_rounded, fill: 1), + onTap: () => RemoteSessionDialog.show(context), + ) + else + ListTile( + leading: const AppIcon(Symbols.phone_android_rounded, fill: 1), + title: const Text('Remote Control'), + subtitle: companionRemote.isConnected + ? Text('Connected to ${companionRemote.connectedDevice?.name}') + : const Text('Control a desktop device'), + trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MobileRemoteScreen()), + ); + }, + ), + ], + ), + ); + }, + ); + } + Widget _buildAdvancedSection() { return Card( child: Column( diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 1ccd18ce..52454e3e 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -22,6 +22,7 @@ import '../models/plex_media_info.dart'; import '../providers/download_provider.dart'; import '../providers/multi_server_provider.dart'; import '../providers/playback_state_provider.dart'; +import '../services/companion_remote/companion_remote_receiver.dart'; import '../services/discord_rpc_service.dart'; import '../services/episode_navigation_service.dart'; import '../services/media_controls_manager.dart'; @@ -38,6 +39,7 @@ import '../providers/shader_provider.dart'; import '../providers/user_profile_provider.dart'; import '../utils/app_logger.dart'; import '../utils/dialogs.dart'; +import '../utils/player_utils.dart'; import '../utils/orientation_helper.dart'; import '../utils/platform_detector.dart'; import '../utils/provider_extensions.dart'; @@ -205,6 +207,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Register app lifecycle observer WidgetsBinding.instance.addObserver(this); + // Wire companion remote playback callbacks + _setupCompanionRemoteCallbacks(); + // Initialize player asynchronously with buffer size from settings _initializePlayer(); } @@ -1125,6 +1130,64 @@ class VideoPlayerScreenState extends State with WidgetsBindin navigateToVideoPlayer(context, metadata: metadata, usePushReplacement: true); } + void _setupCompanionRemoteCallbacks() { + final receiver = CompanionRemoteReceiver.instance; + receiver.onStop = () { + if (mounted) _handleBackButton(); + }; + receiver.onNextTrack = () { + if (mounted && _nextEpisode != null) _playNext(); + }; + receiver.onPreviousTrack = () { + if (mounted && _previousEpisode != null) _playPrevious(); + }; + receiver.onSeekForward = () async { + if (player == null) return; + final settings = await SettingsService.getInstance(); + seekWithClamping(player!, Duration(seconds: settings.getSeekTimeSmall())); + }; + receiver.onSeekBackward = () async { + if (player == null) return; + final settings = await SettingsService.getInstance(); + seekWithClamping(player!, Duration(seconds: -settings.getSeekTimeSmall())); + }; + receiver.onVolumeUp = () async { + if (player == null) return; + final settings = await SettingsService.getInstance(); + final maxVol = settings.getMaxVolume().toDouble(); + final newVolume = (player!.state.volume + 10).clamp(0.0, maxVol); + player!.setVolume(newVolume); + settings.setVolume(newVolume); + }; + receiver.onVolumeDown = () async { + if (player == null) return; + final settings = await SettingsService.getInstance(); + final maxVol = settings.getMaxVolume().toDouble(); + final newVolume = (player!.state.volume - 10).clamp(0.0, maxVol); + player!.setVolume(newVolume); + settings.setVolume(newVolume); + }; + receiver.onVolumeMute = () async { + if (player == null) return; + final settings = await SettingsService.getInstance(); + final newVolume = player!.state.volume > 0 ? 0.0 : 100.0; + player!.setVolume(newVolume); + settings.setVolume(newVolume); + }; + } + + void _cleanupCompanionRemoteCallbacks() { + final receiver = CompanionRemoteReceiver.instance; + receiver.onStop = null; + receiver.onNextTrack = null; + receiver.onPreviousTrack = null; + receiver.onSeekForward = null; + receiver.onSeekBackward = null; + receiver.onVolumeUp = null; + receiver.onVolumeDown = null; + receiver.onVolumeMute = null; + } + /// Handle back button press /// For non-host participants in Watch Together, shows leave session confirmation Future _handleBackButton() async { @@ -1187,6 +1250,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Unregister app lifecycle observer WidgetsBinding.instance.removeObserver(this); + // Clean up companion remote playback callbacks + _cleanupCompanionRemoteCallbacks(); + // Notify Watch Together guests that host is exiting the player // Use stored reference since context.read() may fail in dispose // Skip if replacing with another video (episode navigation) diff --git a/lib/services/companion_remote/companion_remote_discovery_service.dart b/lib/services/companion_remote/companion_remote_discovery_service.dart new file mode 100644 index 00000000..cb3bc716 --- /dev/null +++ b/lib/services/companion_remote/companion_remote_discovery_service.dart @@ -0,0 +1,152 @@ +import 'dart:async'; +import 'dart:convert'; + +import '../../services/storage_service.dart'; +import '../../utils/app_logger.dart'; + +/// Recent Companion Remote session for quick reconnection +class RecentRemoteSession { + final String sessionId; + final String pin; + final String deviceName; + final String platform; + final DateTime lastConnected; + + RecentRemoteSession({ + required this.sessionId, + required this.pin, + required this.deviceName, + required this.platform, + required this.lastConnected, + }); + + factory RecentRemoteSession.fromJson(Map json) { + return RecentRemoteSession( + sessionId: json['sessionId'] as String, + pin: json['pin'] as String, + deviceName: json['deviceName'] as String, + platform: json['platform'] as String, + lastConnected: DateTime.parse(json['lastConnected'] as String), + ); + } + + Map toJson() { + return { + 'sessionId': sessionId, + 'pin': pin, + 'deviceName': deviceName, + 'platform': platform, + 'lastConnected': lastConnected.toIso8601String(), + }; + } + + /// Create from QR code data (format: "sessionId:pin:deviceName:platform") + factory RecentRemoteSession.fromQrData(String qrData) { + final parts = qrData.split(':'); + if (parts.length < 2) { + throw FormatException('Invalid QR code format'); + } + + return RecentRemoteSession( + sessionId: parts[0], + pin: parts[1], + deviceName: parts.length > 2 ? parts[2] : 'Unknown Device', + platform: parts.length > 3 ? parts[3] : 'unknown', + lastConnected: DateTime.now(), + ); + } + + @override + String toString() => '$deviceName ($platform) - Last: ${lastConnected.toLocal()}'; +} + +/// Service for managing recent Companion Remote sessions +class CompanionRemoteDiscoveryService { + static const String _storageKey = 'companion_remote_recent_sessions'; + static const int _maxRecentSessions = 5; + + final _recentSessions = []; + final _recentSessionsController = StreamController>.broadcast(); + + /// Stream of recent sessions + Stream> get recentSessions => _recentSessionsController.stream; + + /// Get current list of recent sessions + List get currentSessions => List.unmodifiable(_recentSessions); + + CompanionRemoteDiscoveryService() { + _loadRecentSessions(); + } + + /// Load recent sessions from storage + Future _loadRecentSessions() async { + try { + final storage = await StorageService.getInstance(); + final json = storage.prefs.getString(_storageKey); + + if (json != null) { + final List list = jsonDecode(json); + _recentSessions.clear(); + _recentSessions.addAll( + list.map((e) => RecentRemoteSession.fromJson(e as Map)), + ); + + // Sort by last connected (most recent first) + _recentSessions.sort((a, b) => b.lastConnected.compareTo(a.lastConnected)); + + _recentSessionsController.add(currentSessions); + appLogger.d('Loaded ${_recentSessions.length} recent remote sessions'); + } + } catch (e) { + appLogger.e('Failed to load recent sessions', error: e); + } + } + + /// Save recent sessions to storage + Future _saveRecentSessions() async { + try { + final storage = await StorageService.getInstance(); + final json = jsonEncode(_recentSessions.map((e) => e.toJson()).toList()); + await storage.prefs.setString(_storageKey, json); + appLogger.d('Saved ${_recentSessions.length} recent remote sessions'); + } catch (e) { + appLogger.e('Failed to save recent sessions', error: e); + } + } + + /// Add a session to recent list + Future addRecentSession(RecentRemoteSession session) async { + // Remove existing entry for this session ID + _recentSessions.removeWhere((s) => s.sessionId == session.sessionId); + + // Add new entry at the beginning + _recentSessions.insert(0, session); + + // Limit to max sessions + if (_recentSessions.length > _maxRecentSessions) { + _recentSessions.removeRange(_maxRecentSessions, _recentSessions.length); + } + + await _saveRecentSessions(); + _recentSessionsController.add(currentSessions); + } + + /// Remove a session from recent list + Future removeRecentSession(String sessionId) async { + _recentSessions.removeWhere((s) => s.sessionId == sessionId); + await _saveRecentSessions(); + _recentSessionsController.add(currentSessions); + } + + /// Clear all recent sessions + Future clearRecentSessions() async { + _recentSessions.clear(); + await _saveRecentSessions(); + _recentSessionsController.add(currentSessions); + } + + /// Dispose resources + Future dispose() async { + await _recentSessionsController.close(); + } +} diff --git a/lib/services/companion_remote/companion_remote_peer_service.dart b/lib/services/companion_remote/companion_remote_peer_service.dart new file mode 100644 index 00000000..442561e6 --- /dev/null +++ b/lib/services/companion_remote/companion_remote_peer_service.dart @@ -0,0 +1,435 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:peerdart/peerdart.dart'; + +import '../../models/companion_remote/remote_command.dart'; +import '../../models/companion_remote/remote_command_type.dart'; +import '../../models/companion_remote/remote_session.dart'; +import '../../utils/app_logger.dart'; + +enum RemotePeerErrorType { + connectionFailed, + peerDisconnected, + dataChannelError, + serverError, + timeout, + invalidSession, + unknown, +} + +class RemotePeerError { + final RemotePeerErrorType type; + final String message; + final dynamic originalError; + + const RemotePeerError({ + required this.type, + required this.message, + this.originalError, + }); + + @override + String toString() => 'RemotePeerError($type): $message'; +} + +class CompanionRemotePeerService { + Peer? _peer; + DataConnection? _connection; + String? _sessionId; + String? _pin; + String? _myPeerId; + RemoteSessionRole? _role; + + final _commandReceivedController = StreamController.broadcast(); + final _deviceConnectedController = StreamController.broadcast(); + final _deviceDisconnectedController = StreamController.broadcast(); + final _errorController = StreamController.broadcast(); + final _connectionStateController = StreamController.broadcast(); + + int _reconnectAttempts = 0; + static const int _maxReconnectAttempts = 3; + Timer? _reconnectTimer; + Timer? _pingTimer; + + Stream get onCommandReceived => _commandReceivedController.stream; + Stream get onDeviceConnected => _deviceConnectedController.stream; + Stream get onDeviceDisconnected => _deviceDisconnectedController.stream; + Stream get onError => _errorController.stream; + Stream get onConnectionStateChanged => _connectionStateController.stream; + + String? get sessionId => _sessionId; + String? get pin => _pin; + String? get myPeerId => _myPeerId; + RemoteSessionRole? get role => _role; + bool get isHost => _role == RemoteSessionRole.host; + bool get isConnected => _connection != null; + + String _generateSessionId() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final random = Random.secure(); + return List.generate(8, (index) => chars[random.nextInt(chars.length)]).join(); + } + + String _generatePin() { + final random = Random.secure(); + return List.generate(6, (index) => random.nextInt(10).toString()).join(); + } + + void _attachCommonPeerListeners({ + required Completer completer, + required RemotePeerErrorType errorType, + required String errorMessage, + }) { + _peer!.on('disconnected').listen((_) { + appLogger.w('CompanionRemote: Peer disconnected from server'); + _handleDisconnectedFromServer(); + }); + + _peer!.on('close').listen((_) { + appLogger.d('CompanionRemote: Peer closed'); + _connectionStateController.add(RemoteSessionStatus.disconnected); + }); + + _peer!.on('error').listen((error) { + appLogger.e('CompanionRemote: Peer error', error: error); + _errorController.add( + RemotePeerError( + type: errorType, + message: '$errorMessage: $error', + originalError: error, + ), + ); + if (!completer.isCompleted) { + completer.completeError(error); + } + }); + } + + Future<({String sessionId, String pin})> createSession(String deviceName, String platform) async { + if (_peer != null) { + await disconnect(); + } + + _role = RemoteSessionRole.host; + _sessionId = _generateSessionId(); + _pin = _generatePin(); + _reconnectAttempts = 0; + + final completer = Completer<({String sessionId, String pin})>(); + + try { + _peer = Peer(id: 'cr-$_sessionId-$_pin'); + + _peer!.on('open').listen((id) { + _myPeerId = id as String; + appLogger.d('CompanionRemote: Host peer opened with ID: $_myPeerId'); + _connectionStateController.add(RemoteSessionStatus.connected); + if (!completer.isCompleted) { + completer.complete((sessionId: _sessionId!, pin: _pin!)); + } + }); + + _peer!.on('connection').listen((conn) { + final dataConn = conn as DataConnection; + _handleNewConnection(dataConn, deviceName, platform); + }); + + _attachCommonPeerListeners( + completer: completer, + errorType: RemotePeerErrorType.serverError, + errorMessage: 'Server error', + ); + } catch (e) { + appLogger.e('CompanionRemote: Failed to create peer', error: e); + if (!completer.isCompleted) { + completer.completeError(e); + } + } + + return completer.future.timeout( + const Duration(seconds: 10), + onTimeout: () { + throw RemotePeerError( + type: RemotePeerErrorType.timeout, + message: 'Timed out creating session', + ); + }, + ); + } + + Future joinSession( + String sessionId, + String pin, + String deviceName, + String platform, + ) async { + if (_peer != null) { + await disconnect(); + } + + _role = RemoteSessionRole.remote; + _sessionId = sessionId.toUpperCase(); + _pin = pin; + _reconnectAttempts = 0; + + final completer = Completer(); + + try { + _peer = Peer(); + + _peer!.on('open').listen((id) { + _myPeerId = id as String; + appLogger.d('CompanionRemote: Remote peer opened with ID: $_myPeerId'); + + final hostPeerId = 'cr-$_sessionId-$_pin'; + appLogger.d('CompanionRemote: Connecting to host: $hostPeerId'); + + _connectionStateController.add(RemoteSessionStatus.connecting); + + final conn = _peer!.connect(hostPeerId, options: PeerConnectOption(reliable: true)); + _handleNewConnection(conn, deviceName, platform, isOutgoing: true, completer: completer); + }); + + _attachCommonPeerListeners( + completer: completer, + errorType: RemotePeerErrorType.connectionFailed, + errorMessage: 'Failed to connect to session', + ); + } catch (e) { + appLogger.e('CompanionRemote: Failed to create peer for joining', error: e); + if (!completer.isCompleted) { + completer.completeError(e); + } + } + + return completer.future.timeout( + const Duration(seconds: 15), + onTimeout: () { + throw RemotePeerError( + type: RemotePeerErrorType.timeout, + message: 'Timed out joining session', + ); + }, + ); + } + + void _handleNewConnection( + DataConnection conn, + String deviceName, + String platform, { + bool isOutgoing = false, + Completer? 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) { + print('🟡 PeerService: DATA RECEIVED! data type: ${data.runtimeType}'); + try { + print('🟡 Parsing as JSON...'); + final json = data as Map; + print('🟡 Creating RemoteCommand from JSON...'); + final command = RemoteCommand.fromJson(json); + print('🟡 Command received: ${command.type} from $peerId'); + 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) { + print('🟡 Sending ACK for ${command.type}...'); + final ackCommand = RemoteCommand( + type: RemoteCommandType.ack, + deviceId: _myPeerId ?? 'unknown', + deviceName: deviceName, + data: {'originalCommand': command.type.toString()}, + ); + _connection?.send(ackCommand.toJson()); + print('🟡 ACK sent!'); + } + + print('🟡 Adding to _commandReceivedController...'); + _commandReceivedController.add(command); + print('🟡 Command added to stream!'); + + if (command.type == RemoteCommandType.ping) { + print('🟡 Responding to ping with pong...'); + _sendPong(deviceName, platform); + } else if (command.type == RemoteCommandType.ack) { + print('✅ RECEIVED ACK from desktop for: ${json['data']?['originalCommand']}'); + } + } catch (e) { + print('🔴 PeerService: Failed to parse command: $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() { + _stopPingTimer(); + _pingTimer = Timer.periodic(const Duration(seconds: 5), (_) { + if (_connection != null) { + sendCommand(RemoteCommand( + type: RemoteCommandType.ping, + deviceId: _myPeerId ?? 'unknown', + deviceName: 'local', + )); + } + }); + } + + void _stopPingTimer() { + _pingTimer?.cancel(); + _pingTimer = null; + } + + void _sendPong(String deviceName, String platform) { + sendCommand(RemoteCommand( + type: RemoteCommandType.pong, + deviceId: _myPeerId ?? 'unknown', + deviceName: deviceName, + data: {'platform': platform}, + )); + } + + void sendDeviceInfo(String deviceName, String platform) { + sendCommand(RemoteCommand( + type: RemoteCommandType.deviceInfo, + deviceId: _myPeerId ?? 'unknown', + deviceName: deviceName, + data: { + 'platform': platform, + 'role': _role?.name, + }, + )); + } + + void sendCommand(RemoteCommand command) { + print('🔵 PeerService sendCommand called! command.type: ${command.type}'); + print('🔵 _connection: $_connection'); + + if (_connection == null) { + print('🔴 PeerService: No connection!'); + appLogger.w('CompanionRemote: No connection to send command'); + return; + } + + try { + print('🔵 Converting command to JSON...'); + final json = command.toJson(); + print('🔵 Sending via connection.send...'); + _connection!.send(json); + print('🔵 Command sent over wire!'); + appLogger.d('CompanionRemote: Sent command: ${command.type}'); + } catch (e) { + print('🔴 PeerService send FAILED: $e'); + appLogger.e('CompanionRemote: Failed to send command', error: e); + _errorController.add( + RemotePeerError( + type: RemotePeerErrorType.dataChannelError, + message: 'Failed to send command: $e', + originalError: e, + ), + ); + } + } + + Future disconnect() async { + appLogger.d('CompanionRemote: Disconnecting'); + + _stopPingTimer(); + _reconnectTimer?.cancel(); + + _connection?.close(); + _connection = null; + + _peer?.dispose(); + _peer = null; + + _sessionId = null; + _pin = null; + _myPeerId = null; + _role = null; + _reconnectAttempts = 0; + + _connectionStateController.add(RemoteSessionStatus.disconnected); + } + + void dispose() { + disconnect(); + _commandReceivedController.close(); + _deviceConnectedController.close(); + _deviceDisconnectedController.close(); + _errorController.close(); + _connectionStateController.close(); + } +} diff --git a/lib/services/companion_remote/companion_remote_receiver.dart b/lib/services/companion_remote/companion_remote_receiver.dart new file mode 100644 index 00000000..fd2caee2 --- /dev/null +++ b/lib/services/companion_remote/companion_remote_receiver.dart @@ -0,0 +1,137 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../../models/companion_remote/remote_command.dart'; +import '../../models/companion_remote/remote_command_type.dart'; +import '../../utils/app_logger.dart'; +import '../../utils/key_event_simulator.dart'; + +class CompanionRemoteReceiver { + CompanionRemoteReceiver._(); + + static CompanionRemoteReceiver? _instance; + + static CompanionRemoteReceiver get instance { + _instance ??= CompanionRemoteReceiver._(); + return _instance!; + } + + /// Called on any remote input so InputModeTracker can switch to keyboard mode. + /// Same pattern as [GamepadService.onGamepadInput]. + static VoidCallback? onRemoteInput; + + VoidCallback? onTabNext; + VoidCallback? onTabPrevious; + VoidCallback? onTabDiscover; + VoidCallback? onTabLibraries; + VoidCallback? onTabSearch; + VoidCallback? onTabDownloads; + VoidCallback? onTabSettings; + VoidCallback? onHome; + void Function(String? query)? onSearchAction; + VoidCallback? onNextTrack; + VoidCallback? onPreviousTrack; + VoidCallback? onStop; + VoidCallback? onSeekForward; + VoidCallback? onSeekBackward; + VoidCallback? onVolumeUp; + VoidCallback? onVolumeDown; + VoidCallback? onVolumeMute; + + void handleCommand(RemoteCommand command, BuildContext? context) { + appLogger.d('CompanionRemoteReceiver: Handling command: ${command.type}'); + + // Switch to keyboard mode so focus visuals render + onRemoteInput?.call(); + _setTraditionalFocusHighlight(); + scheduleFrameIfIdle(); + + switch (command.type) { + case RemoteCommandType.dpadUp: + simulateKeyPress(LogicalKeyboardKey.arrowUp); + case RemoteCommandType.dpadDown: + simulateKeyPress(LogicalKeyboardKey.arrowDown); + case RemoteCommandType.dpadLeft: + simulateKeyPress(LogicalKeyboardKey.arrowLeft); + case RemoteCommandType.dpadRight: + simulateKeyPress(LogicalKeyboardKey.arrowRight); + case RemoteCommandType.select: + simulateKeyPress(LogicalKeyboardKey.enter); + case RemoteCommandType.back: + simulateKeyPress(LogicalKeyboardKey.escape); + case RemoteCommandType.contextMenu: + simulateKeyPress(LogicalKeyboardKey.contextMenu); + + case RemoteCommandType.play: + simulateKeyPress(LogicalKeyboardKey.space); + case RemoteCommandType.pause: + simulateKeyPress(LogicalKeyboardKey.space); + case RemoteCommandType.playPause: + simulateKeyPress(LogicalKeyboardKey.space); + case RemoteCommandType.seekForward: + onSeekForward?.call(); + case RemoteCommandType.seekBackward: + onSeekBackward?.call(); + + case RemoteCommandType.volumeUp: + onVolumeUp?.call(); + case RemoteCommandType.volumeDown: + onVolumeDown?.call(); + case RemoteCommandType.volumeMute: + onVolumeMute?.call(); + + case RemoteCommandType.tabNext: + onTabNext?.call(); + case RemoteCommandType.tabPrevious: + onTabPrevious?.call(); + case RemoteCommandType.tabDiscover: + onTabDiscover?.call(); + case RemoteCommandType.tabLibraries: + onTabLibraries?.call(); + case RemoteCommandType.tabSearch: + onTabSearch?.call(); + case RemoteCommandType.tabDownloads: + onTabDownloads?.call(); + case RemoteCommandType.tabSettings: + onTabSettings?.call(); + + case RemoteCommandType.home: + onHome?.call(); + case RemoteCommandType.search: + final query = command.data?['query'] as String?; + onSearchAction?.call(query); + + case RemoteCommandType.stop: + onStop?.call(); + case RemoteCommandType.nextTrack: + onNextTrack?.call(); + case RemoteCommandType.previousTrack: + onPreviousTrack?.call(); + + case RemoteCommandType.subtitles: + case RemoteCommandType.audioTracks: + break; // No-op: track cycling not yet implemented + + case RemoteCommandType.fullscreen: + simulateKeyPress(LogicalKeyboardKey.keyF); + + case RemoteCommandType.ping: + case RemoteCommandType.pong: + case RemoteCommandType.ack: + case RemoteCommandType.deviceInfo: + case RemoteCommandType.capabilitiesRequest: + case RemoteCommandType.capabilitiesResponse: + case RemoteCommandType.disconnect: + break; + + default: + appLogger.w('CompanionRemoteReceiver: Unhandled command type: ${command.type}'); + } + } + + void _setTraditionalFocusHighlight() { + if (FocusManager.instance.highlightStrategy != FocusHighlightStrategy.alwaysTraditional) { + FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + } + } +} diff --git a/lib/services/gamepad_service.dart b/lib/services/gamepad_service.dart index 83d776a8..78fe94c8 100644 --- a/lib/services/gamepad_service.dart +++ b/lib/services/gamepad_service.dart @@ -2,11 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:universal_gamepad/universal_gamepad.dart'; import '../utils/app_logger.dart'; +import '../utils/key_event_simulator.dart' as key_sim; /// Service that bridges gamepad input to Flutter's focus navigation system. /// @@ -106,7 +107,7 @@ class GamepadService { if (event.pressed) { onGamepadInput?.call(); _setTraditionalFocusHighlight(); - _scheduleFrameIfIdle(); + key_sim.scheduleFrameIfIdle(); } final wasPressed = _pressedButtons.contains(event.button); @@ -170,7 +171,7 @@ class GamepadService { if (event.value.abs() > 0.3) { onGamepadInput?.call(); _setTraditionalFocusHighlight(); - _scheduleFrameIfIdle(); + SchedulerBinding.instance.ensureVisualUpdate(); } switch (event.axis) { @@ -343,12 +344,4 @@ class GamepadService { } } - // Force a frame when the engine is idle so focus visuals update immediately - // on gamepad input (desktop may not wake up without mouse/keyboard activity). - void _scheduleFrameIfIdle() { - final scheduler = SchedulerBinding.instance; - if (scheduler.schedulerPhase == SchedulerPhase.idle) { - scheduler.scheduleFrame(); - } - } } diff --git a/lib/utils/key_event_simulator.dart b/lib/utils/key_event_simulator.dart new file mode 100644 index 00000000..9b187a66 --- /dev/null +++ b/lib/utils/key_event_simulator.dart @@ -0,0 +1,76 @@ +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Shared utility for simulating key press events through the focus tree. +/// +/// Used by both [CompanionRemoteReceiver] and [GamepadService] to translate +/// external input (remote commands, gamepad buttons) into focus-tree key events. +void simulateKeyPress(LogicalKeyboardKey logicalKey) { + SchedulerBinding.instance.addPostFrameCallback((_) { + final focusNode = FocusManager.instance.primaryFocus; + if (focusNode == null) return; + + final physicalKey = _getPhysicalKey(logicalKey); + + final keyDownEvent = KeyDownEvent( + physicalKey: physicalKey, + logicalKey: logicalKey, + timeStamp: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch), + ); + + // Walk up the focus tree dispatching the key event + FocusNode? node = focusNode; + KeyEventResult result = KeyEventResult.ignored; + + while (node != null && result != KeyEventResult.handled) { + if (node.onKeyEvent != null) { + result = node.onKeyEvent!(node, keyDownEvent); + } + node = node.parent; + } + + // Send key up event + final keyUpEvent = KeyUpEvent( + physicalKey: physicalKey, + logicalKey: logicalKey, + timeStamp: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch), + ); + + node = focusNode; + while (node != null) { + if (node.onKeyEvent != null) { + final upResult = node.onKeyEvent!(node, keyUpEvent); + if (upResult == KeyEventResult.handled) break; + } + node = node.parent; + } + }); +} + +/// Force a frame when the engine is idle so focus visuals update immediately +/// on external input (desktop may not wake up without mouse/keyboard activity). +void scheduleFrameIfIdle() { + if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { + SchedulerBinding.instance.scheduleFrame(); + } +} + +PhysicalKeyboardKey _getPhysicalKey(LogicalKeyboardKey logicalKey) { + if (logicalKey == LogicalKeyboardKey.arrowUp) return PhysicalKeyboardKey.arrowUp; + if (logicalKey == LogicalKeyboardKey.arrowDown) return PhysicalKeyboardKey.arrowDown; + if (logicalKey == LogicalKeyboardKey.arrowLeft) return PhysicalKeyboardKey.arrowLeft; + if (logicalKey == LogicalKeyboardKey.arrowRight) return PhysicalKeyboardKey.arrowRight; + if (logicalKey == LogicalKeyboardKey.enter) return PhysicalKeyboardKey.enter; + if (logicalKey == LogicalKeyboardKey.escape) return PhysicalKeyboardKey.escape; + if (logicalKey == LogicalKeyboardKey.space) return PhysicalKeyboardKey.space; + if (logicalKey == LogicalKeyboardKey.contextMenu) return PhysicalKeyboardKey.contextMenu; + if (logicalKey == LogicalKeyboardKey.audioVolumeUp) return PhysicalKeyboardKey.audioVolumeUp; + if (logicalKey == LogicalKeyboardKey.audioVolumeDown) return PhysicalKeyboardKey.audioVolumeDown; + if (logicalKey == LogicalKeyboardKey.audioVolumeMute) return PhysicalKeyboardKey.audioVolumeMute; + if (logicalKey == LogicalKeyboardKey.keyF) return PhysicalKeyboardKey.keyF; + if (logicalKey == LogicalKeyboardKey.gameButtonA) return PhysicalKeyboardKey.gameButtonA; + if (logicalKey == LogicalKeyboardKey.gameButtonB) return PhysicalKeyboardKey.gameButtonB; + if (logicalKey == LogicalKeyboardKey.gameButtonX) return PhysicalKeyboardKey.gameButtonX; + return PhysicalKeyboardKey.enter; +} diff --git a/lib/widgets/companion_remote/remote_session_dialog.dart b/lib/widgets/companion_remote/remote_session_dialog.dart new file mode 100644 index 00000000..f9e0f967 --- /dev/null +++ b/lib/widgets/companion_remote/remote_session_dialog.dart @@ -0,0 +1,335 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +import '../../models/companion_remote/remote_session.dart'; +import '../../providers/companion_remote_provider.dart'; +import '../../utils/app_logger.dart'; + +class RemoteSessionDialog extends StatefulWidget { + const RemoteSessionDialog({super.key}); + + @override + State createState() => _RemoteSessionDialogState(); + + static Future show(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const RemoteSessionDialog(), + ); + } +} + +class _RemoteSessionDialogState extends State { + bool _isCreatingSession = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _createSession(); + } + + Future _createSession() async { + setState(() { + _isCreatingSession = true; + _errorMessage = null; + }); + + try { + final provider = context.read(); + await provider.createSession(); + + setState(() { + _isCreatingSession = false; + }); + } catch (e) { + appLogger.e('Failed to create companion remote session', error: e); + setState(() { + _isCreatingSession = false; + _errorMessage = e.toString(); + }); + } + } + + void _copyToClipboard(String text, String label) { + Clipboard.setData(ClipboardData(text: text)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$label copied to clipboard'), + duration: const Duration(seconds: 2), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + if (_isCreatingSession) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text( + 'Creating remote session...', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + ), + ); + } + + if (_errorMessage != null) { + return AlertDialog( + title: const Text('Error'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Failed to create remote session:'), + const SizedBox(height: 8), + Text( + _errorMessage!, + style: const TextStyle(fontFamily: 'monospace'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + TextButton( + onPressed: _createSession, + child: const Text('Retry'), + ), + ], + ); + } + + final session = provider.session; + if (session == null) { + return AlertDialog( + title: const Text('Error'), + content: const Text('No session available'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ); + } + + final qrData = '${session.sessionId}:${session.pin}'; + + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const Icon(Icons.phone_android, size: 32), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Companion Remote', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 4), + Text( + session.connectedDevice != null + ? 'Connected to ${session.connectedDevice!.name}' + : 'Waiting for connection...', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: session.connectedDevice != null + ? Colors.green + : Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + const SizedBox(height: 24), + if (session.connectedDevice == null) ...[ + Text( + 'Scan QR Code', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Center( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: QrImageView( + data: qrData, + version: QrVersions.auto, + size: 200, + backgroundColor: Colors.white, + ), + ), + ), + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 16), + Text( + 'Or enter manually', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + _buildCodeCard( + context, + 'Session ID', + session.sessionId, + onCopy: () => _copyToClipboard(session.sessionId, 'Session ID'), + ), + const SizedBox(height: 12), + _buildCodeCard( + context, + 'PIN', + session.pin, + onCopy: () => _copyToClipboard(session.pin, 'PIN'), + ), + ] else ...[ + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Icon( + Icons.check_circle, + color: Colors.green, + size: 48, + ), + const SizedBox(height: 8), + Text( + 'Connected', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + session.connectedDevice!.name, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + session.connectedDevice!.platform, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Use your mobile device to control this app', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ], + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (session.connectedDevice != null) + TextButton.icon( + onPressed: () async { + await provider.leaveSession(); + await _createSession(); + }, + icon: const Icon(Icons.refresh), + label: const Text('New Session'), + ), + const SizedBox(width: 8), + TextButton( + onPressed: () async { + await provider.leaveSession(); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: const Text('Disconnect'), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Minimize'), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildCodeCard( + BuildContext context, + String label, + String code, { + VoidCallback? onCopy, + }) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 4), + Text( + code, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontFamily: 'monospace', + letterSpacing: 2, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.copy), + onPressed: onCopy, + tooltip: 'Copy to clipboard', + ), + ], + ), + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 0a25aa35..69e7208d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -14,6 +15,9 @@ #include 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 = fl_plugin_registry_get_registrar_for_plugin(registry, "OsMediaControlsPlugin"); os_media_controls_plugin_register_with_registrar(os_media_controls_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index eb699977..a560d685 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_webrtc os_media_controls screen_retriever_linux sqlite3_flutter_libs diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9ec69bfa..dc700d78 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,9 +8,12 @@ import Foundation import connectivity_plus import device_info_plus import file_picker +import flutter_webrtc import in_app_review +import mobile_scanner import os_media_controls import package_info_plus +import path_provider_foundation import screen_retriever_macos import shared_preferences_foundation import sqflite_darwin @@ -24,9 +27,12 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ade972c6..b2650411 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,13 +5,21 @@ PODS: - FlutterMacOS - file_picker (0.0.1): - FlutterMacOS + - flutter_webrtc (1.2.0): + - FlutterMacOS + - WebRTC-SDK (= 137.7151.04) - FlutterMacOS (1.0.0) - in_app_review (2.0.0): - FlutterMacOS + - mobile_scanner (6.0.2): + - FlutterMacOS - os_media_controls (0.0.1): - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS - screen_retriever_macos (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -51,17 +59,21 @@ PODS: - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS - - window_manager (0.5.0): + - WebRTC-SDK (137.7151.04) + - window_manager (0.2.0): - FlutterMacOS DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/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`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - os_media_controls (from `Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) @@ -74,6 +86,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - sqlite3 + - WebRTC-SDK EXTERNAL SOURCES: connectivity_plus: @@ -82,14 +95,20 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + flutter_webrtc: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos FlutterMacOS: :path: Flutter/ephemeral in_app_review: :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos + mobile_scanner: + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos os_media_controls: :path: Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin screen_retriever_macos: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos shared_preferences_foundation: @@ -111,10 +130,13 @@ SPEC CHECKSUMS: connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4 + mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 @@ -123,7 +145,8 @@ SPEC CHECKSUMS: universal_gamepad: 8922f1f238f62d6847de887228976d5b572b57da url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b - window_manager: b729e31d38fb04905235df9ea896128991cad99e + WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d diff --git a/pubspec.lock b/pubspec.lock index c9b3df52..442ce73b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "91.0.0" + version: "85.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "7.7.1" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "825071d553c4aef2252196d46a665fbd8e0cb06de07725f25d1b29bd18d65fff" + sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "0.13.4" ansicolor: dependency: transitive description: @@ -69,18 +69,18 @@ packages: dependency: transitive description: name: build - sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "2.5.4" build_config: dependency: transitive description: name: build_config - sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.1.2" build_daemon: dependency: transitive description: @@ -89,14 +89,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: ac78098de97893812b7aff1154f29008fa2464cad9e8e7044d39bc905dad4fbc + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" built_collection: dependency: transitive description: @@ -109,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d url: "https://pub.dev" source: hosted - version: "8.12.3" + version: "8.12.0" cached_network_image: dependency: "direct main" description: @@ -137,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + chalkdart: + dependency: transitive + description: + name: chalkdart + sha256: "7ffc6bd39c81453fb9ba8dbce042a9c960219b75ea1c07196a7fa41c2fab9e86" + url: "https://pub.dev" + source: hosted + version: "3.0.5" characters: dependency: transitive description: @@ -177,22 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" code_builder: dependency: transitive description: name: code_builder - sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" url: "https://pub.dev" source: hosted - version: "4.11.1" + version: "4.11.0" collection: dependency: transitive description: @@ -229,10 +245,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" url: "https://pub.dev" source: hosted - version: "0.3.5+2" + version: "0.3.5+1" crypto: dependency: "direct main" description: @@ -261,10 +277,10 @@ packages: dependency: "direct dev" description: name: dart_code_linter - sha256: "1b53722d9933a5f5d4580acc29c7f16b1fde66d21d1ecf7bb2a811caf3a42b42" + sha256: "9456e0a7508b0d76be301dc2f73be37e601019e85c5f59e6a5cc460af90f7dcd" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.1.1" dart_discord_presence: dependency: "direct main" description: @@ -277,18 +293,26 @@ packages: dependency: transitive description: name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" url: "https://pub.dev" source: hosted - version: "3.1.3" + 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: dependency: transitive description: name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.11" device_info_plus: dependency: "direct main" description: @@ -309,10 +333,10 @@ packages: dependency: "direct main" description: name: dio - sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 url: "https://pub.dev" source: hosted - version: "5.9.1" + version: "5.9.0" dio_web_adapter: dependency: transitive description: @@ -325,18 +349,18 @@ packages: dependency: "direct main" description: name: drift - sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e" + sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1" url: "https://pub.dev" source: hosted - version: "2.31.0" + version: "2.28.2" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587" + sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4" url: "https://pub.dev" source: hosted - version: "2.31.0" + version: "2.28.0" duration: dependency: "direct main" description: @@ -345,6 +369,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -357,10 +389,10 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.4" file: dependency: transitive description: @@ -389,18 +421,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: a0979dd61f21b634717b98eb4ceaed2bfe009fe020ce8597aaf164b9eeb57aaa + sha256: f5b0b53d4ae0d59b1e28dfc21d5398e5028cf8e764518e491a52fd050aa23881 url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.7.2" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: a3183753bbcfc3af106224bff3ab3e1844b73f58062136b7499919f49f3667e7 + sha256: "828291a5a4d4283590541519d8b57821946660ac61d2e07d955f81cfcab22e5d" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "3.6.1" flutter: dependency: "direct main" description: flutter @@ -461,6 +493,22 @@ packages: description: flutter source: sdk 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: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" glob: dependency: transitive description: @@ -477,14 +525,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" html: dependency: transitive description: @@ -521,10 +561,10 @@ packages: dependency: transitive description: name: image - sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.7.2" + version: "4.5.4" in_app_review: dependency: "direct main" description: @@ -557,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -569,10 +617,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.11.2" + version: "6.9.5" leak_tracker: dependency: transitive description: @@ -641,10 +689,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 + sha256: "02555a48e1ec02b16e532dfd4ef13c4f6bf7ec7c20230e58e56641a393433dc3" url: "https://pub.dev" source: hosted - version: "4.2906.0" + version: "4.2892.0" meta: dependency: transitive description: @@ -661,14 +709,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive + mobile_scanner: + dependency: "direct main" description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + name: mobile_scanner + sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" url: "https://pub.dev" source: hosted - version: "0.17.4" + version: "6.0.11" nested: dependency: transitive description: @@ -685,14 +733,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" octo_image: dependency: transitive description: @@ -762,18 +802,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16 url: "https://pub.dev" source: hosted - version: "2.2.22" + version: "2.2.20" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.4.3" path_provider_linux: dependency: transitive description: @@ -798,6 +838,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + peerdart: + dependency: "direct main" + description: + name: peerdart + sha256: "1d0db041d42194f42e57d8889d029fb8d107610bf1062b39f43f4651de547e3c" + url: "https://pub.dev" + source: hosted + version: "0.5.6" petitparser: dependency: transitive description: @@ -914,10 +962,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" url: "https://pub.dev" source: hosted - version: "0.28.0" + version: "0.27.7" saf_stream: dependency: "direct main" description: @@ -978,26 +1026,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.4.15" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.5" shared_preferences_linux: dependency: transitive description: @@ -1055,50 +1103,50 @@ packages: dependency: "direct main" description: name: slang - sha256: "81e277dc5e2305f53412b92afeb803620453259afe147d5cd6417700f998a7a6" + sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2" url: "https://pub.dev" source: hosted - version: "4.12.1" + version: "4.12.0" slang_build_runner: dependency: "direct dev" description: name: slang_build_runner - sha256: "010f337703dbc736ac9f0e3eb9d6e1464850736fed69f85a23c07ee4678fcc1c" + sha256: "453d74b5430153a3c4150d5ba8f6380e0785f3939f7511f10ac5b6cf9bb7d2a7" url: "https://pub.dev" source: hosted - version: "4.12.1" + version: "4.12.0" slang_flutter: dependency: "direct main" description: name: slang_flutter - sha256: "0f0276c400660c8b67150005aa4df57643b86ce6ae9c824abee8e25f345d9abc" + sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0" url: "https://pub.dev" source: hosted - version: "4.12.1" + version: "4.12.0" source_gen: dependency: transitive description: name: source_gen - sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "2.0.0" source_helper: dependency: transitive description: name: source_helper - sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca url: "https://pub.dev" source: hosted - version: "1.3.8" + version: "1.3.7" source_span: dependency: transitive description: name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.2" + version: "1.10.1" sqflite: dependency: transitive description: @@ -1159,10 +1207,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19" + sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67" url: "https://pub.dev" source: hosted - version: "0.43.1" + version: "0.41.2" stack_trace: dependency: transitive description: @@ -1219,6 +1267,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: @@ -1247,34 +1303,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9" url: "https://pub.dev" source: hosted - version: "6.3.28" + version: "6.3.24" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9" url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.3.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.4" url_launcher_platform_interface: dependency: transitive description: @@ -1287,18 +1343,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1367,10 +1423,10 @@ packages: dependency: transitive description: name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.4" web: dependency: transitive description: @@ -1395,6 +1451,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -1477,4 +1541,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4d087375..4c939f30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,11 @@ dependencies: in_app_review: ^2.0.11 dart_discord_presence: ^1.1.0 flutter_svg: ^2.2.3 + mobile_scanner: ^6.0.2 + peerdart: ^0.5.6 + +dependency_overrides: + flutter_webrtc: ^1.3.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bc3ebff2..88e1c798 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FlutterWebRTCPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); OsMediaControlsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index db77f229..1dca6fac 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus + flutter_webrtc os_media_controls screen_retriever_windows sqlite3_flutter_libs From 972ded5b0cd37f19f5676e540c65ca6bc178f992 Mon Sep 17 00:00:00 2001 From: Matt Vogel Date: Mon, 9 Feb 2026 14:20:32 -0500 Subject: [PATCH 2/8] Fix remote session dialog, peer service, and mobile remote screen --- .../mobile_remote_screen.dart | 3 --- .../companion_remote_peer_service.dart | 20 +------------------ .../remote_session_dialog.dart | 1 - 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/lib/screens/companion_remote/mobile_remote_screen.dart b/lib/screens/companion_remote/mobile_remote_screen.dart index 7afe049a..53f164dc 100644 --- a/lib/screens/companion_remote/mobile_remote_screen.dart +++ b/lib/screens/companion_remote/mobile_remote_screen.dart @@ -176,12 +176,9 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { void _sendCommand(RemoteCommandType type) { HapticFeedback.lightImpact(); - print('🔴 MobileRemoteScreen: Button pressed! Type: $type'); appLogger.d('MobileRemoteScreen: Sending command: $type'); final provider = context.read(); - print('🔴 Provider isConnected: ${provider.isConnected}'); provider.sendCommand(type); - print('🔴 sendCommand called'); } @override diff --git a/lib/services/companion_remote/companion_remote_peer_service.dart b/lib/services/companion_remote/companion_remote_peer_service.dart index 442561e6..72dfccbd 100644 --- a/lib/services/companion_remote/companion_remote_peer_service.dart +++ b/lib/services/companion_remote/companion_remote_peer_service.dart @@ -247,13 +247,9 @@ class CompanionRemotePeerService { }); conn.on('data').listen((data) { - print('🟡 PeerService: DATA RECEIVED! data type: ${data.runtimeType}'); try { - print('🟡 Parsing as JSON...'); final json = data as Map; - print('🟡 Creating RemoteCommand from JSON...'); final command = RemoteCommand.fromJson(json); - print('🟡 Command received: ${command.type} from $peerId'); appLogger.d('CompanionRemote: Received command: ${command.type} from $peerId'); // Send acknowledgment for non-ping/pong/ack commands @@ -261,7 +257,6 @@ class CompanionRemotePeerService { command.type != RemoteCommandType.pong && command.type != RemoteCommandType.ack && command.type != RemoteCommandType.deviceInfo) { - print('🟡 Sending ACK for ${command.type}...'); final ackCommand = RemoteCommand( type: RemoteCommandType.ack, deviceId: _myPeerId ?? 'unknown', @@ -269,21 +264,16 @@ class CompanionRemotePeerService { data: {'originalCommand': command.type.toString()}, ); _connection?.send(ackCommand.toJson()); - print('🟡 ACK sent!'); } - print('🟡 Adding to _commandReceivedController...'); _commandReceivedController.add(command); - print('🟡 Command added to stream!'); if (command.type == RemoteCommandType.ping) { - print('🟡 Responding to ping with pong...'); _sendPong(deviceName, platform); } else if (command.type == RemoteCommandType.ack) { - print('✅ RECEIVED ACK from desktop for: ${json['data']?['originalCommand']}'); + appLogger.d('CompanionRemote: Received ACK for: ${json['data']?['originalCommand']}'); } } catch (e) { - print('🔴 PeerService: Failed to parse command: $e'); appLogger.e('CompanionRemote: Failed to parse command', error: e); } }); @@ -374,24 +364,16 @@ class CompanionRemotePeerService { } void sendCommand(RemoteCommand command) { - print('🔵 PeerService sendCommand called! command.type: ${command.type}'); - print('🔵 _connection: $_connection'); - if (_connection == null) { - print('🔴 PeerService: No connection!'); appLogger.w('CompanionRemote: No connection to send command'); return; } try { - print('🔵 Converting command to JSON...'); final json = command.toJson(); - print('🔵 Sending via connection.send...'); _connection!.send(json); - print('🔵 Command sent over wire!'); appLogger.d('CompanionRemote: Sent command: ${command.type}'); } catch (e) { - print('🔴 PeerService send FAILED: $e'); appLogger.e('CompanionRemote: Failed to send command', error: e); _errorController.add( RemotePeerError( diff --git a/lib/widgets/companion_remote/remote_session_dialog.dart b/lib/widgets/companion_remote/remote_session_dialog.dart index f9e0f967..08f2b027 100644 --- a/lib/widgets/companion_remote/remote_session_dialog.dart +++ b/lib/widgets/companion_remote/remote_session_dialog.dart @@ -3,7 +3,6 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import '../../models/companion_remote/remote_session.dart'; import '../../providers/companion_remote_provider.dart'; import '../../utils/app_logger.dart'; From 199d634cb2a8962131d7fe44798abfbf4cb69fb2 Mon Sep 17 00:00:00 2001 From: Matt Vogel Date: Mon, 9 Feb 2026 14:21:01 -0500 Subject: [PATCH 3/8] Migrate companion remote models to @JsonSerializable --- lib/database/app_database.g.dart | 1522 ++++++++++++----- lib/i18n/strings.g.dart | 2 +- .../recent_remote_session.dart | 44 + .../recent_remote_session.g.dart | 23 + .../companion_remote/remote_command.dart | 43 +- .../companion_remote/remote_command.g.dart | 67 + .../companion_remote/remote_session.dart | 81 +- .../companion_remote/remote_session.g.dart | 61 + .../companion_remote/trusted_device.dart | 31 +- .../companion_remote/trusted_device.g.dart | 25 + lib/models/play_queue_response.g.dart | 32 +- lib/models/plex_library.g.dart | 25 +- lib/models/plex_metadata.g.dart | 95 +- lib/models/plex_playlist.g.dart | 39 +- lib/providers/companion_remote_provider.dart | 83 +- .../companion_remote/pairing_screen.dart | 121 +- .../companion_remote_discovery_service.dart | 61 +- macos/Podfile.lock | 4 +- 18 files changed, 1540 insertions(+), 819 deletions(-) create mode 100644 lib/models/companion_remote/recent_remote_session.dart create mode 100644 lib/models/companion_remote/recent_remote_session.g.dart create mode 100644 lib/models/companion_remote/remote_command.g.dart create mode 100644 lib/models/companion_remote/remote_session.g.dart create mode 100644 lib/models/companion_remote/trusted_device.g.dart diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 6844c0e6..db269630 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -3,7 +3,8 @@ part of 'app_database.dart'; // ignore_for_file: type=lint -class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMediaTable, DownloadedMediaItem> { +class $DownloadedMediaTable extends DownloadedMedia + with TableInfo<$DownloadedMediaTable, DownloadedMediaItem> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -17,9 +18,13 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', ); - static const VerificationMeta _serverIdMeta = const VerificationMeta('serverId'); @override late final GeneratedColumn serverId = GeneratedColumn( 'server_id', @@ -28,7 +33,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _ratingKeyMeta = const VerificationMeta('ratingKey'); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta( + 'ratingKey', + ); @override late final GeneratedColumn ratingKey = GeneratedColumn( 'rating_key', @@ -37,7 +44,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _globalKeyMeta = const VerificationMeta('globalKey'); + static const VerificationMeta _globalKeyMeta = const VerificationMeta( + 'globalKey', + ); @override late final GeneratedColumn globalKey = GeneratedColumn( 'global_key', @@ -56,7 +65,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _parentRatingKeyMeta = const VerificationMeta('parentRatingKey'); + static const VerificationMeta _parentRatingKeyMeta = const VerificationMeta( + 'parentRatingKey', + ); @override late final GeneratedColumn parentRatingKey = GeneratedColumn( 'parent_rating_key', @@ -65,15 +76,17 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _grandparentRatingKeyMeta = const VerificationMeta('grandparentRatingKey'); + static const VerificationMeta _grandparentRatingKeyMeta = + const VerificationMeta('grandparentRatingKey'); @override - late final GeneratedColumn grandparentRatingKey = GeneratedColumn( - 'grandparent_rating_key', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); + late final GeneratedColumn grandparentRatingKey = + GeneratedColumn( + 'grandparent_rating_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); static const VerificationMeta _statusMeta = const VerificationMeta('status'); @override late final GeneratedColumn status = GeneratedColumn( @@ -83,7 +96,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _progressMeta = const VerificationMeta('progress'); + static const VerificationMeta _progressMeta = const VerificationMeta( + 'progress', + ); @override late final GeneratedColumn progress = GeneratedColumn( 'progress', @@ -93,7 +108,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _totalBytesMeta = const VerificationMeta('totalBytes'); + static const VerificationMeta _totalBytesMeta = const VerificationMeta( + 'totalBytes', + ); @override late final GeneratedColumn totalBytes = GeneratedColumn( 'total_bytes', @@ -102,7 +119,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _downloadedBytesMeta = const VerificationMeta('downloadedBytes'); + static const VerificationMeta _downloadedBytesMeta = const VerificationMeta( + 'downloadedBytes', + ); @override late final GeneratedColumn downloadedBytes = GeneratedColumn( 'downloaded_bytes', @@ -112,7 +131,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _videoFilePathMeta = const VerificationMeta('videoFilePath'); + static const VerificationMeta _videoFilePathMeta = const VerificationMeta( + 'videoFilePath', + ); @override late final GeneratedColumn videoFilePath = GeneratedColumn( 'video_file_path', @@ -121,7 +142,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _thumbPathMeta = const VerificationMeta('thumbPath'); + static const VerificationMeta _thumbPathMeta = const VerificationMeta( + 'thumbPath', + ); @override late final GeneratedColumn thumbPath = GeneratedColumn( 'thumb_path', @@ -130,7 +153,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _downloadedAtMeta = const VerificationMeta('downloadedAt'); + static const VerificationMeta _downloadedAtMeta = const VerificationMeta( + 'downloadedAt', + ); @override late final GeneratedColumn downloadedAt = GeneratedColumn( 'downloaded_at', @@ -139,7 +164,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _errorMessageMeta = const VerificationMeta('errorMessage'); + static const VerificationMeta _errorMessageMeta = const VerificationMeta( + 'errorMessage', + ); @override late final GeneratedColumn errorMessage = GeneratedColumn( 'error_message', @@ -148,7 +175,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _retryCountMeta = const VerificationMeta('retryCount'); + static const VerificationMeta _retryCountMeta = const VerificationMeta( + 'retryCount', + ); @override late final GeneratedColumn retryCount = GeneratedColumn( 'retry_count', @@ -183,78 +212,132 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe String get actualTableName => $name; static const String $name = 'downloaded_media'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } if (data.containsKey('server_id')) { - context.handle(_serverIdMeta, serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta)); + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); } else if (isInserting) { context.missing(_serverIdMeta); } if (data.containsKey('rating_key')) { - context.handle(_ratingKeyMeta, ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta)); + context.handle( + _ratingKeyMeta, + ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), + ); } else if (isInserting) { context.missing(_ratingKeyMeta); } if (data.containsKey('global_key')) { - context.handle(_globalKeyMeta, globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta)); + context.handle( + _globalKeyMeta, + globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), + ); } else if (isInserting) { context.missing(_globalKeyMeta); } if (data.containsKey('type')) { - context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); } else if (isInserting) { context.missing(_typeMeta); } if (data.containsKey('parent_rating_key')) { context.handle( _parentRatingKeyMeta, - parentRatingKey.isAcceptableOrUnknown(data['parent_rating_key']!, _parentRatingKeyMeta), + parentRatingKey.isAcceptableOrUnknown( + data['parent_rating_key']!, + _parentRatingKeyMeta, + ), ); } if (data.containsKey('grandparent_rating_key')) { context.handle( _grandparentRatingKeyMeta, - grandparentRatingKey.isAcceptableOrUnknown(data['grandparent_rating_key']!, _grandparentRatingKeyMeta), + grandparentRatingKey.isAcceptableOrUnknown( + data['grandparent_rating_key']!, + _grandparentRatingKeyMeta, + ), ); } if (data.containsKey('status')) { - context.handle(_statusMeta, status.isAcceptableOrUnknown(data['status']!, _statusMeta)); + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); } else if (isInserting) { context.missing(_statusMeta); } if (data.containsKey('progress')) { - context.handle(_progressMeta, progress.isAcceptableOrUnknown(data['progress']!, _progressMeta)); + context.handle( + _progressMeta, + progress.isAcceptableOrUnknown(data['progress']!, _progressMeta), + ); } if (data.containsKey('total_bytes')) { - context.handle(_totalBytesMeta, totalBytes.isAcceptableOrUnknown(data['total_bytes']!, _totalBytesMeta)); + context.handle( + _totalBytesMeta, + totalBytes.isAcceptableOrUnknown(data['total_bytes']!, _totalBytesMeta), + ); } if (data.containsKey('downloaded_bytes')) { context.handle( _downloadedBytesMeta, - downloadedBytes.isAcceptableOrUnknown(data['downloaded_bytes']!, _downloadedBytesMeta), + downloadedBytes.isAcceptableOrUnknown( + data['downloaded_bytes']!, + _downloadedBytesMeta, + ), ); } if (data.containsKey('video_file_path')) { context.handle( _videoFilePathMeta, - videoFilePath.isAcceptableOrUnknown(data['video_file_path']!, _videoFilePathMeta), + videoFilePath.isAcceptableOrUnknown( + data['video_file_path']!, + _videoFilePathMeta, + ), ); } if (data.containsKey('thumb_path')) { - context.handle(_thumbPathMeta, thumbPath.isAcceptableOrUnknown(data['thumb_path']!, _thumbPathMeta)); + context.handle( + _thumbPathMeta, + thumbPath.isAcceptableOrUnknown(data['thumb_path']!, _thumbPathMeta), + ); } if (data.containsKey('downloaded_at')) { - context.handle(_downloadedAtMeta, downloadedAt.isAcceptableOrUnknown(data['downloaded_at']!, _downloadedAtMeta)); + context.handle( + _downloadedAtMeta, + downloadedAt.isAcceptableOrUnknown( + data['downloaded_at']!, + _downloadedAtMeta, + ), + ); } if (data.containsKey('error_message')) { - context.handle(_errorMessageMeta, errorMessage.isAcceptableOrUnknown(data['error_message']!, _errorMessageMeta)); + context.handle( + _errorMessageMeta, + errorMessage.isAcceptableOrUnknown( + data['error_message']!, + _errorMessageMeta, + ), + ); } if (data.containsKey('retry_count')) { - context.handle(_retryCountMeta, retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta)); + context.handle( + _retryCountMeta, + retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta), + ); } return context; } @@ -265,11 +348,26 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe DownloadedMediaItem map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return DownloadedMediaItem( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - serverId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}server_id'])!, - ratingKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}rating_key'])!, - globalKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}global_key'])!, - type: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}type'])!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ratingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}rating_key'], + )!, + globalKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}global_key'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, parentRatingKey: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}parent_rating_key'], @@ -278,15 +376,42 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe DriftSqlType.string, data['${effectivePrefix}grandparent_rating_key'], ), - status: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}status'])!, - progress: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}progress'])!, - totalBytes: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}total_bytes']), - downloadedBytes: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}downloaded_bytes'])!, - videoFilePath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}video_file_path']), - thumbPath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumb_path']), - downloadedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}downloaded_at']), - errorMessage: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}error_message']), - retryCount: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}retry_count'])!, + status: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}status'], + )!, + progress: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}progress'], + )!, + totalBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}total_bytes'], + ), + downloadedBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}downloaded_bytes'], + )!, + videoFilePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}video_file_path'], + ), + thumbPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_path'], + ), + downloadedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}downloaded_at'], + ), + errorMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}error_message'], + ), + retryCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}retry_count'], + )!, ); } @@ -296,7 +421,8 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe } } -class DownloadedMediaItem extends DataClass implements Insertable { +class DownloadedMediaItem extends DataClass + implements Insertable { final int id; final String serverId; final String ratingKey; @@ -374,23 +500,38 @@ class DownloadedMediaItem extends DataClass implements Insertable json, {ValueSerializer? serializer}) { + factory DownloadedMediaItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return DownloadedMediaItem( id: serializer.fromJson(json['id']), @@ -399,7 +540,9 @@ class DownloadedMediaItem extends DataClass implements Insertable(json['globalKey']), type: serializer.fromJson(json['type']), parentRatingKey: serializer.fromJson(json['parentRatingKey']), - grandparentRatingKey: serializer.fromJson(json['grandparentRatingKey']), + grandparentRatingKey: serializer.fromJson( + json['grandparentRatingKey'], + ), status: serializer.fromJson(json['status']), progress: serializer.fromJson(json['progress']), totalBytes: serializer.fromJson(json['totalBytes']), @@ -457,13 +600,19 @@ class DownloadedMediaItem extends DataClass implements Insertable { if (globalKey != null) 'global_key': globalKey, if (type != null) 'type': type, if (parentRatingKey != null) 'parent_rating_key': parentRatingKey, - if (grandparentRatingKey != null) 'grandparent_rating_key': grandparentRatingKey, + if (grandparentRatingKey != null) + 'grandparent_rating_key': grandparentRatingKey, if (status != null) 'status': status, if (progress != null) 'progress': progress, if (totalBytes != null) 'total_bytes': totalBytes, @@ -711,7 +875,9 @@ class DownloadedMediaCompanion extends UpdateCompanion { map['parent_rating_key'] = Variable(parentRatingKey.value); } if (grandparentRatingKey.present) { - map['grandparent_rating_key'] = Variable(grandparentRatingKey.value); + map['grandparent_rating_key'] = Variable( + grandparentRatingKey.value, + ); } if (status.present) { map['status'] = Variable(status.value); @@ -767,7 +933,8 @@ class DownloadedMediaCompanion extends UpdateCompanion { } } -class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTable, DownloadQueueItem> { +class $DownloadQueueTable extends DownloadQueue + with TableInfo<$DownloadQueueTable, DownloadQueueItem> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -781,9 +948,13 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _mediaGlobalKeyMeta = const VerificationMeta( + 'mediaGlobalKey', ); - static const VerificationMeta _mediaGlobalKeyMeta = const VerificationMeta('mediaGlobalKey'); @override late final GeneratedColumn mediaGlobalKey = GeneratedColumn( 'media_global_key', @@ -793,7 +964,9 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), ); - static const VerificationMeta _priorityMeta = const VerificationMeta('priority'); + static const VerificationMeta _priorityMeta = const VerificationMeta( + 'priority', + ); @override late final GeneratedColumn priority = GeneratedColumn( 'priority', @@ -803,7 +976,9 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _addedAtMeta = const VerificationMeta('addedAt'); + static const VerificationMeta _addedAtMeta = const VerificationMeta( + 'addedAt', + ); @override late final GeneratedColumn addedAt = GeneratedColumn( 'added_at', @@ -812,7 +987,9 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _downloadSubtitlesMeta = const VerificationMeta('downloadSubtitles'); + static const VerificationMeta _downloadSubtitlesMeta = const VerificationMeta( + 'downloadSubtitles', + ); @override late final GeneratedColumn downloadSubtitles = GeneratedColumn( 'download_subtitles', @@ -820,10 +997,14 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("download_subtitles" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("download_subtitles" IN (0, 1))', + ), defaultValue: const Constant(true), ); - static const VerificationMeta _downloadArtworkMeta = const VerificationMeta('downloadArtwork'); + static const VerificationMeta _downloadArtworkMeta = const VerificationMeta( + 'downloadArtwork', + ); @override late final GeneratedColumn downloadArtwork = GeneratedColumn( 'download_artwork', @@ -831,18 +1012,30 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("download_artwork" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("download_artwork" IN (0, 1))', + ), defaultValue: const Constant(true), ); @override - List get $columns => [id, mediaGlobalKey, priority, addedAt, downloadSubtitles, downloadArtwork]; + List get $columns => [ + id, + mediaGlobalKey, + priority, + addedAt, + downloadSubtitles, + downloadArtwork, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'download_queue'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -851,29 +1044,44 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab if (data.containsKey('media_global_key')) { context.handle( _mediaGlobalKeyMeta, - mediaGlobalKey.isAcceptableOrUnknown(data['media_global_key']!, _mediaGlobalKeyMeta), + mediaGlobalKey.isAcceptableOrUnknown( + data['media_global_key']!, + _mediaGlobalKeyMeta, + ), ); } else if (isInserting) { context.missing(_mediaGlobalKeyMeta); } if (data.containsKey('priority')) { - context.handle(_priorityMeta, priority.isAcceptableOrUnknown(data['priority']!, _priorityMeta)); + context.handle( + _priorityMeta, + priority.isAcceptableOrUnknown(data['priority']!, _priorityMeta), + ); } if (data.containsKey('added_at')) { - context.handle(_addedAtMeta, addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta)); + context.handle( + _addedAtMeta, + addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta), + ); } else if (isInserting) { context.missing(_addedAtMeta); } if (data.containsKey('download_subtitles')) { context.handle( _downloadSubtitlesMeta, - downloadSubtitles.isAcceptableOrUnknown(data['download_subtitles']!, _downloadSubtitlesMeta), + downloadSubtitles.isAcceptableOrUnknown( + data['download_subtitles']!, + _downloadSubtitlesMeta, + ), ); } if (data.containsKey('download_artwork')) { context.handle( _downloadArtworkMeta, - downloadArtwork.isAcceptableOrUnknown(data['download_artwork']!, _downloadArtworkMeta), + downloadArtwork.isAcceptableOrUnknown( + data['download_artwork']!, + _downloadArtworkMeta, + ), ); } return context; @@ -885,13 +1093,22 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab DownloadQueueItem map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return DownloadQueueItem( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, mediaGlobalKey: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}media_global_key'], )!, - priority: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}priority'])!, - addedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}added_at'])!, + priority: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}priority'], + )!, + addedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}added_at'], + )!, downloadSubtitles: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}download_subtitles'], @@ -909,7 +1126,8 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab } } -class DownloadQueueItem extends DataClass implements Insertable { +class DownloadQueueItem extends DataClass + implements Insertable { final int id; final String mediaGlobalKey; final int priority; @@ -947,7 +1165,10 @@ class DownloadQueueItem extends DataClass implements Insertable json, {ValueSerializer? serializer}) { + factory DownloadQueueItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return DownloadQueueItem( id: serializer.fromJson(json['id']), @@ -989,11 +1210,17 @@ class DownloadQueueItem extends DataClass implements Insertable Object.hash(id, mediaGlobalKey, priority, addedAt, downloadSubtitles, downloadArtwork); + int get hashCode => Object.hash( + id, + mediaGlobalKey, + priority, + addedAt, + downloadSubtitles, + downloadArtwork, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -1122,12 +1356,15 @@ class DownloadQueueCompanion extends UpdateCompanion { } } -class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheData> { +class $ApiCacheTable extends ApiCache + with TableInfo<$ApiCacheTable, ApiCacheData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; $ApiCacheTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _cacheKeyMeta = const VerificationMeta('cacheKey'); + static const VerificationMeta _cacheKeyMeta = const VerificationMeta( + 'cacheKey', + ); @override late final GeneratedColumn cacheKey = GeneratedColumn( 'cache_key', @@ -1153,10 +1390,14 @@ class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheDat false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pinned" IN (0, 1))', + ), defaultValue: const Constant(false), ); - static const VerificationMeta _cachedAtMeta = const VerificationMeta('cachedAt'); + static const VerificationMeta _cachedAtMeta = const VerificationMeta( + 'cachedAt', + ); @override late final GeneratedColumn cachedAt = GeneratedColumn( 'cached_at', @@ -1174,24 +1415,39 @@ class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheDat String get actualTableName => $name; static const String $name = 'api_cache'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('cache_key')) { - context.handle(_cacheKeyMeta, cacheKey.isAcceptableOrUnknown(data['cache_key']!, _cacheKeyMeta)); + context.handle( + _cacheKeyMeta, + cacheKey.isAcceptableOrUnknown(data['cache_key']!, _cacheKeyMeta), + ); } else if (isInserting) { context.missing(_cacheKeyMeta); } if (data.containsKey('data')) { - context.handle(_dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); + context.handle( + _dataMeta, + this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), + ); } else if (isInserting) { context.missing(_dataMeta); } if (data.containsKey('pinned')) { - context.handle(_pinnedMeta, pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + context.handle( + _pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta), + ); } if (data.containsKey('cached_at')) { - context.handle(_cachedAtMeta, cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta)); + context.handle( + _cachedAtMeta, + cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta), + ); } return context; } @@ -1202,10 +1458,22 @@ class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheDat ApiCacheData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ApiCacheData( - cacheKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}cache_key'])!, - data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!, - pinned: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, - cachedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}cached_at'])!, + cacheKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cache_key'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + pinned: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pinned'], + )!, + cachedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}cached_at'], + )!, ); } @@ -1227,7 +1495,12 @@ class ApiCacheData extends DataClass implements Insertable { /// Timestamp for cache invalidation (optional future use) final DateTime cachedAt; - const ApiCacheData({required this.cacheKey, required this.data, required this.pinned, required this.cachedAt}); + const ApiCacheData({ + required this.cacheKey, + required this.data, + required this.pinned, + required this.cachedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1247,7 +1520,10 @@ class ApiCacheData extends DataClass implements Insertable { ); } - factory ApiCacheData.fromJson(Map json, {ValueSerializer? serializer}) { + factory ApiCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ApiCacheData( cacheKey: serializer.fromJson(json['cacheKey']), @@ -1267,7 +1543,12 @@ class ApiCacheData extends DataClass implements Insertable { }; } - ApiCacheData copyWith({String? cacheKey, String? data, bool? pinned, DateTime? cachedAt}) => ApiCacheData( + ApiCacheData copyWith({ + String? cacheKey, + String? data, + bool? pinned, + DateTime? cachedAt, + }) => ApiCacheData( cacheKey: cacheKey ?? this.cacheKey, data: data ?? this.data, pinned: pinned ?? this.pinned, @@ -1407,9 +1688,13 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', ); - static const VerificationMeta _serverIdMeta = const VerificationMeta('serverId'); @override late final GeneratedColumn serverId = GeneratedColumn( 'server_id', @@ -1418,7 +1703,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _ratingKeyMeta = const VerificationMeta('ratingKey'); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta( + 'ratingKey', + ); @override late final GeneratedColumn ratingKey = GeneratedColumn( 'rating_key', @@ -1427,7 +1714,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _globalKeyMeta = const VerificationMeta('globalKey'); + static const VerificationMeta _globalKeyMeta = const VerificationMeta( + 'globalKey', + ); @override late final GeneratedColumn globalKey = GeneratedColumn( 'global_key', @@ -1436,7 +1725,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _actionTypeMeta = const VerificationMeta('actionType'); + static const VerificationMeta _actionTypeMeta = const VerificationMeta( + 'actionType', + ); @override late final GeneratedColumn actionType = GeneratedColumn( 'action_type', @@ -1445,7 +1736,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _viewOffsetMeta = const VerificationMeta('viewOffset'); + static const VerificationMeta _viewOffsetMeta = const VerificationMeta( + 'viewOffset', + ); @override late final GeneratedColumn viewOffset = GeneratedColumn( 'view_offset', @@ -1454,7 +1747,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _durationMeta = const VerificationMeta('duration'); + static const VerificationMeta _durationMeta = const VerificationMeta( + 'duration', + ); @override late final GeneratedColumn duration = GeneratedColumn( 'duration', @@ -1463,7 +1758,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _shouldMarkWatchedMeta = const VerificationMeta('shouldMarkWatched'); + static const VerificationMeta _shouldMarkWatchedMeta = const VerificationMeta( + 'shouldMarkWatched', + ); @override late final GeneratedColumn shouldMarkWatched = GeneratedColumn( 'should_mark_watched', @@ -1471,10 +1768,14 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("should_mark_watched" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("should_mark_watched" IN (0, 1))', + ), defaultValue: const Constant(false), ); - static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); @override late final GeneratedColumn createdAt = GeneratedColumn( 'created_at', @@ -1483,7 +1784,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); @override late final GeneratedColumn updatedAt = GeneratedColumn( 'updated_at', @@ -1492,7 +1795,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _syncAttemptsMeta = const VerificationMeta('syncAttempts'); + static const VerificationMeta _syncAttemptsMeta = const VerificationMeta( + 'syncAttempts', + ); @override late final GeneratedColumn syncAttempts = GeneratedColumn( 'sync_attempts', @@ -1502,7 +1807,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _lastErrorMeta = const VerificationMeta('lastError'); + static const VerificationMeta _lastErrorMeta = const VerificationMeta( + 'lastError', + ); @override late final GeneratedColumn lastError = GeneratedColumn( 'last_error', @@ -1532,59 +1839,98 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress String get actualTableName => $name; static const String $name = 'offline_watch_progress'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } if (data.containsKey('server_id')) { - context.handle(_serverIdMeta, serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta)); + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); } else if (isInserting) { context.missing(_serverIdMeta); } if (data.containsKey('rating_key')) { - context.handle(_ratingKeyMeta, ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta)); + context.handle( + _ratingKeyMeta, + ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), + ); } else if (isInserting) { context.missing(_ratingKeyMeta); } if (data.containsKey('global_key')) { - context.handle(_globalKeyMeta, globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta)); + context.handle( + _globalKeyMeta, + globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), + ); } else if (isInserting) { context.missing(_globalKeyMeta); } if (data.containsKey('action_type')) { - context.handle(_actionTypeMeta, actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta)); + context.handle( + _actionTypeMeta, + actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta), + ); } else if (isInserting) { context.missing(_actionTypeMeta); } if (data.containsKey('view_offset')) { - context.handle(_viewOffsetMeta, viewOffset.isAcceptableOrUnknown(data['view_offset']!, _viewOffsetMeta)); + context.handle( + _viewOffsetMeta, + viewOffset.isAcceptableOrUnknown(data['view_offset']!, _viewOffsetMeta), + ); } if (data.containsKey('duration')) { - context.handle(_durationMeta, duration.isAcceptableOrUnknown(data['duration']!, _durationMeta)); + context.handle( + _durationMeta, + duration.isAcceptableOrUnknown(data['duration']!, _durationMeta), + ); } if (data.containsKey('should_mark_watched')) { context.handle( _shouldMarkWatchedMeta, - shouldMarkWatched.isAcceptableOrUnknown(data['should_mark_watched']!, _shouldMarkWatchedMeta), + shouldMarkWatched.isAcceptableOrUnknown( + data['should_mark_watched']!, + _shouldMarkWatchedMeta, + ), ); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); } else if (isInserting) { context.missing(_createdAtMeta); } if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); } else if (isInserting) { context.missing(_updatedAtMeta); } if (data.containsKey('sync_attempts')) { - context.handle(_syncAttemptsMeta, syncAttempts.isAcceptableOrUnknown(data['sync_attempts']!, _syncAttemptsMeta)); + context.handle( + _syncAttemptsMeta, + syncAttempts.isAcceptableOrUnknown( + data['sync_attempts']!, + _syncAttemptsMeta, + ), + ); } if (data.containsKey('last_error')) { - context.handle(_lastErrorMeta, lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta)); + context.handle( + _lastErrorMeta, + lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), + ); } return context; } @@ -1592,24 +1938,60 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress @override Set get $primaryKey => {id}; @override - OfflineWatchProgressItem map(Map data, {String? tablePrefix}) { + OfflineWatchProgressItem map( + Map data, { + String? tablePrefix, + }) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return OfflineWatchProgressItem( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - serverId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}server_id'])!, - ratingKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}rating_key'])!, - globalKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}global_key'])!, - actionType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}action_type'])!, - viewOffset: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}view_offset']), - duration: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration']), + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ratingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}rating_key'], + )!, + globalKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}global_key'], + )!, + actionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}action_type'], + )!, + viewOffset: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}view_offset'], + ), + duration: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration'], + ), shouldMarkWatched: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}should_mark_watched'], )!, - createdAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}created_at'])!, - updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}updated_at'])!, - syncAttempts: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}sync_attempts'])!, - lastError: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}last_error']), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + syncAttempts: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sync_attempts'], + )!, + lastError: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_error'], + ), ); } @@ -1619,7 +2001,8 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress } } -class OfflineWatchProgressItem extends DataClass implements Insertable { +class OfflineWatchProgressItem extends DataClass + implements Insertable { /// Auto-incrementing primary key final int id; @@ -1701,17 +2084,26 @@ class OfflineWatchProgressItem extends DataClass implements Insertable json, {ValueSerializer? serializer}) { + factory OfflineWatchProgressItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return OfflineWatchProgressItem( id: serializer.fromJson(json['id']), @@ -1774,19 +2166,29 @@ class OfflineWatchProgressItem extends DataClass implements Insertable { +class OfflineWatchProgressCompanion + extends UpdateCompanion { final Value id; final Value serverId; final Value ratingKey; @@ -2014,14 +2417,23 @@ class OfflineWatchProgressCompanion extends UpdateCompanion $AppDatabaseManager(this); - late final $DownloadedMediaTable downloadedMedia = $DownloadedMediaTable(this); + late final $DownloadedMediaTable downloadedMedia = $DownloadedMediaTable( + this, + ); late final $DownloadQueueTable downloadQueue = $DownloadQueueTable(this); late final $ApiCacheTable apiCache = $ApiCacheTable(this); - late final $OfflineWatchProgressTable offlineWatchProgress = $OfflineWatchProgressTable(this); + late final $OfflineWatchProgressTable offlineWatchProgress = + $OfflineWatchProgressTable(this); @override - Iterable> get allTables => allSchemaEntities.whereType>(); + Iterable> get allTables => + allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [downloadedMedia, downloadQueue, apiCache, offlineWatchProgress]; + List get allSchemaEntities => [ + downloadedMedia, + downloadQueue, + apiCache, + offlineWatchProgress, + ]; } typedef $$DownloadedMediaTableCreateCompanionBuilder = @@ -2063,7 +2475,8 @@ typedef $$DownloadedMediaTableUpdateCompanionBuilder = Value retryCount, }); -class $$DownloadedMediaTableFilterComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableFilterComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableFilterComposer({ required super.$db, required super.$table, @@ -2071,54 +2484,89 @@ class $$DownloadedMediaTableFilterComposer extends Composer<_$AppDatabase, $Down super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnFilters(column)); + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get type => $composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get parentRatingKey => - $composableBuilder(column: $table.parentRatingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get grandparentRatingKey => - $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get status => - $composableBuilder(column: $table.status, builder: (column) => ColumnFilters(column)); + ColumnFilters get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get progress => - $composableBuilder(column: $table.progress, builder: (column) => ColumnFilters(column)); + ColumnFilters get progress => $composableBuilder( + column: $table.progress, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get totalBytes => - $composableBuilder(column: $table.totalBytes, builder: (column) => ColumnFilters(column)); + ColumnFilters get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadedBytes => - $composableBuilder(column: $table.downloadedBytes, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get videoFilePath => - $composableBuilder(column: $table.videoFilePath, builder: (column) => ColumnFilters(column)); + ColumnFilters get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get thumbPath => - $composableBuilder(column: $table.thumbPath, builder: (column) => ColumnFilters(column)); + ColumnFilters get thumbPath => $composableBuilder( + column: $table.thumbPath, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadedAt => - $composableBuilder(column: $table.downloadedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get errorMessage => - $composableBuilder(column: $table.errorMessage, builder: (column) => ColumnFilters(column)); + ColumnFilters get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get retryCount => - $composableBuilder(column: $table.retryCount, builder: (column) => ColumnFilters(column)); + ColumnFilters get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnFilters(column), + ); } -class $$DownloadedMediaTableOrderingComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableOrderingComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableOrderingComposer({ required super.$db, required super.$table, @@ -2126,55 +2574,89 @@ class $$DownloadedMediaTableOrderingComposer extends Composer<_$AppDatabase, $Do super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get type => - $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get parentRatingKey => - $composableBuilder(column: $table.parentRatingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get grandparentRatingKey => - $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get status => - $composableBuilder(column: $table.status, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get progress => - $composableBuilder(column: $table.progress, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get progress => $composableBuilder( + column: $table.progress, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get totalBytes => - $composableBuilder(column: $table.totalBytes, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadedBytes => - $composableBuilder(column: $table.downloadedBytes, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get videoFilePath => - $composableBuilder(column: $table.videoFilePath, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get thumbPath => - $composableBuilder(column: $table.thumbPath, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get thumbPath => $composableBuilder( + column: $table.thumbPath, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadedAt => - $composableBuilder(column: $table.downloadedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get errorMessage => - $composableBuilder(column: $table.errorMessage, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get retryCount => - $composableBuilder(column: $table.retryCount, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnOrderings(column), + ); } -class $$DownloadedMediaTableAnnotationComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableAnnotationComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableAnnotationComposer({ required super.$db, required super.$table, @@ -2182,42 +2664,69 @@ class $$DownloadedMediaTableAnnotationComposer extends Composer<_$AppDatabase, $ super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get serverId => $composableBuilder(column: $table.serverId, builder: (column) => column); + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); - GeneratedColumn get ratingKey => $composableBuilder(column: $table.ratingKey, builder: (column) => column); + GeneratedColumn get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => column); - GeneratedColumn get globalKey => $composableBuilder(column: $table.globalKey, builder: (column) => column); + GeneratedColumn get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => column); - GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); - GeneratedColumn get parentRatingKey => - $composableBuilder(column: $table.parentRatingKey, builder: (column) => column); + GeneratedColumn get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => column, + ); - GeneratedColumn get grandparentRatingKey => - $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => column); + GeneratedColumn get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => column, + ); - GeneratedColumn get status => $composableBuilder(column: $table.status, builder: (column) => column); + GeneratedColumn get status => + $composableBuilder(column: $table.status, builder: (column) => column); - GeneratedColumn get progress => $composableBuilder(column: $table.progress, builder: (column) => column); + GeneratedColumn get progress => + $composableBuilder(column: $table.progress, builder: (column) => column); - GeneratedColumn get totalBytes => $composableBuilder(column: $table.totalBytes, builder: (column) => column); + GeneratedColumn get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => column, + ); - GeneratedColumn get downloadedBytes => - $composableBuilder(column: $table.downloadedBytes, builder: (column) => column); + GeneratedColumn get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => column, + ); - GeneratedColumn get videoFilePath => - $composableBuilder(column: $table.videoFilePath, builder: (column) => column); + GeneratedColumn get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => column, + ); - GeneratedColumn get thumbPath => $composableBuilder(column: $table.thumbPath, builder: (column) => column); + GeneratedColumn get thumbPath => + $composableBuilder(column: $table.thumbPath, builder: (column) => column); - GeneratedColumn get downloadedAt => $composableBuilder(column: $table.downloadedAt, builder: (column) => column); + GeneratedColumn get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => column, + ); - GeneratedColumn get errorMessage => - $composableBuilder(column: $table.errorMessage, builder: (column) => column); + GeneratedColumn get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => column, + ); - GeneratedColumn get retryCount => $composableBuilder(column: $table.retryCount, builder: (column) => column); + GeneratedColumn get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => column, + ); } class $$DownloadedMediaTableTableManager @@ -2231,18 +2740,30 @@ class $$DownloadedMediaTableTableManager $$DownloadedMediaTableAnnotationComposer, $$DownloadedMediaTableCreateCompanionBuilder, $$DownloadedMediaTableUpdateCompanionBuilder, - (DownloadedMediaItem, BaseReferences<_$AppDatabase, $DownloadedMediaTable, DownloadedMediaItem>), + ( + DownloadedMediaItem, + BaseReferences< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem + >, + ), DownloadedMediaItem, PrefetchHooks Function() > { - $$DownloadedMediaTableTableManager(_$AppDatabase db, $DownloadedMediaTable table) - : super( + $$DownloadedMediaTableTableManager( + _$AppDatabase db, + $DownloadedMediaTable table, + ) : super( TableManagerState( db: db, table: table, - createFilteringComposer: () => $$DownloadedMediaTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$DownloadedMediaTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$DownloadedMediaTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$DownloadedMediaTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DownloadedMediaTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DownloadedMediaTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -2315,7 +2836,9 @@ class $$DownloadedMediaTableTableManager errorMessage: errorMessage, retryCount: retryCount, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2331,7 +2854,14 @@ typedef $$DownloadedMediaTableProcessedTableManager = $$DownloadedMediaTableAnnotationComposer, $$DownloadedMediaTableCreateCompanionBuilder, $$DownloadedMediaTableUpdateCompanionBuilder, - (DownloadedMediaItem, BaseReferences<_$AppDatabase, $DownloadedMediaTable, DownloadedMediaItem>), + ( + DownloadedMediaItem, + BaseReferences< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem + >, + ), DownloadedMediaItem, PrefetchHooks Function() >; @@ -2354,7 +2884,8 @@ typedef $$DownloadQueueTableUpdateCompanionBuilder = Value downloadArtwork, }); -class $$DownloadQueueTableFilterComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableFilterComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableFilterComposer({ required super.$db, required super.$table, @@ -2362,25 +2893,39 @@ class $$DownloadQueueTableFilterComposer extends Composer<_$AppDatabase, $Downlo super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get mediaGlobalKey => - $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get priority => - $composableBuilder(column: $table.priority, builder: (column) => ColumnFilters(column)); + ColumnFilters get priority => $composableBuilder( + column: $table.priority, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get addedAt => - $composableBuilder(column: $table.addedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get addedAt => $composableBuilder( + column: $table.addedAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadSubtitles => - $composableBuilder(column: $table.downloadSubtitles, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadArtwork => - $composableBuilder(column: $table.downloadArtwork, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => ColumnFilters(column), + ); } -class $$DownloadQueueTableOrderingComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableOrderingComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableOrderingComposer({ required super.$db, required super.$table, @@ -2388,25 +2933,39 @@ class $$DownloadQueueTableOrderingComposer extends Composer<_$AppDatabase, $Down super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get mediaGlobalKey => - $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get priority => - $composableBuilder(column: $table.priority, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get priority => $composableBuilder( + column: $table.priority, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get addedAt => - $composableBuilder(column: $table.addedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get addedAt => $composableBuilder( + column: $table.addedAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadSubtitles => - $composableBuilder(column: $table.downloadSubtitles, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadArtwork => - $composableBuilder(column: $table.downloadArtwork, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => ColumnOrderings(column), + ); } -class $$DownloadQueueTableAnnotationComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableAnnotationComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableAnnotationComposer({ required super.$db, required super.$table, @@ -2414,20 +2973,29 @@ class $$DownloadQueueTableAnnotationComposer extends Composer<_$AppDatabase, $Do super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get mediaGlobalKey => - $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => column); + GeneratedColumn get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => column, + ); - GeneratedColumn get priority => $composableBuilder(column: $table.priority, builder: (column) => column); + GeneratedColumn get priority => + $composableBuilder(column: $table.priority, builder: (column) => column); - GeneratedColumn get addedAt => $composableBuilder(column: $table.addedAt, builder: (column) => column); + GeneratedColumn get addedAt => + $composableBuilder(column: $table.addedAt, builder: (column) => column); - GeneratedColumn get downloadSubtitles => - $composableBuilder(column: $table.downloadSubtitles, builder: (column) => column); + GeneratedColumn get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => column, + ); - GeneratedColumn get downloadArtwork => - $composableBuilder(column: $table.downloadArtwork, builder: (column) => column); + GeneratedColumn get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => column, + ); } class $$DownloadQueueTableTableManager @@ -2441,7 +3009,14 @@ class $$DownloadQueueTableTableManager $$DownloadQueueTableAnnotationComposer, $$DownloadQueueTableCreateCompanionBuilder, $$DownloadQueueTableUpdateCompanionBuilder, - (DownloadQueueItem, BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>), + ( + DownloadQueueItem, + BaseReferences< + _$AppDatabase, + $DownloadQueueTable, + DownloadQueueItem + >, + ), DownloadQueueItem, PrefetchHooks Function() > { @@ -2450,9 +3025,12 @@ class $$DownloadQueueTableTableManager TableManagerState( db: db, table: table, - createFilteringComposer: () => $$DownloadQueueTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$DownloadQueueTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$DownloadQueueTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$DownloadQueueTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DownloadQueueTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DownloadQueueTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -2485,7 +3063,9 @@ class $$DownloadQueueTableTableManager downloadSubtitles: downloadSubtitles, downloadArtwork: downloadArtwork, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2501,7 +3081,10 @@ typedef $$DownloadQueueTableProcessedTableManager = $$DownloadQueueTableAnnotationComposer, $$DownloadQueueTableCreateCompanionBuilder, $$DownloadQueueTableUpdateCompanionBuilder, - (DownloadQueueItem, BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>), + ( + DownloadQueueItem, + BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>, + ), DownloadQueueItem, PrefetchHooks Function() >; @@ -2522,7 +3105,8 @@ typedef $$ApiCacheTableUpdateCompanionBuilder = Value rowid, }); -class $$ApiCacheTableFilterComposer extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableFilterComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableFilterComposer({ required super.$db, required super.$table, @@ -2530,19 +3114,29 @@ class $$ApiCacheTableFilterComposer extends Composer<_$AppDatabase, $ApiCacheTab super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get cacheKey => - $composableBuilder(column: $table.cacheKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get cacheKey => $composableBuilder( + column: $table.cacheKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get data => $composableBuilder(column: $table.data, builder: (column) => ColumnFilters(column)); + ColumnFilters get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinned => $composableBuilder( + column: $table.pinned, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get cachedAt => - $composableBuilder(column: $table.cachedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get cachedAt => $composableBuilder( + column: $table.cachedAt, + builder: (column) => ColumnFilters(column), + ); } -class $$ApiCacheTableOrderingComposer extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableOrderingComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableOrderingComposer({ required super.$db, required super.$table, @@ -2550,20 +3144,29 @@ class $$ApiCacheTableOrderingComposer extends Composer<_$AppDatabase, $ApiCacheT super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get cacheKey => - $composableBuilder(column: $table.cacheKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get cacheKey => $composableBuilder( + column: $table.cacheKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get data => - $composableBuilder(column: $table.data, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinned => $composableBuilder( + column: $table.pinned, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get cachedAt => - $composableBuilder(column: $table.cachedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get cachedAt => $composableBuilder( + column: $table.cachedAt, + builder: (column) => ColumnOrderings(column), + ); } -class $$ApiCacheTableAnnotationComposer extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableAnnotationComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableAnnotationComposer({ required super.$db, required super.$table, @@ -2571,13 +3174,17 @@ class $$ApiCacheTableAnnotationComposer extends Composer<_$AppDatabase, $ApiCach super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get cacheKey => $composableBuilder(column: $table.cacheKey, builder: (column) => column); + GeneratedColumn get cacheKey => + $composableBuilder(column: $table.cacheKey, builder: (column) => column); - GeneratedColumn get data => $composableBuilder(column: $table.data, builder: (column) => column); + GeneratedColumn get data => + $composableBuilder(column: $table.data, builder: (column) => column); - GeneratedColumn get pinned => $composableBuilder(column: $table.pinned, builder: (column) => column); + GeneratedColumn get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => column); - GeneratedColumn get cachedAt => $composableBuilder(column: $table.cachedAt, builder: (column) => column); + GeneratedColumn get cachedAt => + $composableBuilder(column: $table.cachedAt, builder: (column) => column); } class $$ApiCacheTableTableManager @@ -2591,7 +3198,10 @@ class $$ApiCacheTableTableManager $$ApiCacheTableAnnotationComposer, $$ApiCacheTableCreateCompanionBuilder, $$ApiCacheTableUpdateCompanionBuilder, - (ApiCacheData, BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>), + ( + ApiCacheData, + BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, + ), ApiCacheData, PrefetchHooks Function() > { @@ -2600,9 +3210,12 @@ class $$ApiCacheTableTableManager TableManagerState( db: db, table: table, - createFilteringComposer: () => $$ApiCacheTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$ApiCacheTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$ApiCacheTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$ApiCacheTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ApiCacheTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ApiCacheTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value cacheKey = const Value.absent(), @@ -2610,7 +3223,13 @@ class $$ApiCacheTableTableManager Value pinned = const Value.absent(), Value cachedAt = const Value.absent(), Value rowid = const Value.absent(), - }) => ApiCacheCompanion(cacheKey: cacheKey, data: data, pinned: pinned, cachedAt: cachedAt, rowid: rowid), + }) => ApiCacheCompanion( + cacheKey: cacheKey, + data: data, + pinned: pinned, + cachedAt: cachedAt, + rowid: rowid, + ), createCompanionCallback: ({ required String cacheKey, @@ -2625,7 +3244,9 @@ class $$ApiCacheTableTableManager cachedAt: cachedAt, rowid: rowid, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2641,7 +3262,10 @@ typedef $$ApiCacheTableProcessedTableManager = $$ApiCacheTableAnnotationComposer, $$ApiCacheTableCreateCompanionBuilder, $$ApiCacheTableUpdateCompanionBuilder, - (ApiCacheData, BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>), + ( + ApiCacheData, + BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, + ), ApiCacheData, PrefetchHooks Function() >; @@ -2676,7 +3300,8 @@ typedef $$OfflineWatchProgressTableUpdateCompanionBuilder = Value lastError, }); -class $$OfflineWatchProgressTableFilterComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableFilterComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableFilterComposer({ required super.$db, required super.$table, @@ -2684,43 +3309,69 @@ class $$OfflineWatchProgressTableFilterComposer extends Composer<_$AppDatabase, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnFilters(column)); + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get actionType => - $composableBuilder(column: $table.actionType, builder: (column) => ColumnFilters(column)); + ColumnFilters get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get viewOffset => - $composableBuilder(column: $table.viewOffset, builder: (column) => ColumnFilters(column)); + ColumnFilters get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get duration => - $composableBuilder(column: $table.duration, builder: (column) => ColumnFilters(column)); + ColumnFilters get duration => $composableBuilder( + column: $table.duration, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get shouldMarkWatched => - $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => ColumnFilters(column)); + ColumnFilters get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get syncAttempts => - $composableBuilder(column: $table.syncAttempts, builder: (column) => ColumnFilters(column)); + ColumnFilters get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get lastError => - $composableBuilder(column: $table.lastError, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnFilters(column), + ); } -class $$OfflineWatchProgressTableOrderingComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableOrderingComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableOrderingComposer({ required super.$db, required super.$table, @@ -2728,43 +3379,69 @@ class $$OfflineWatchProgressTableOrderingComposer extends Composer<_$AppDatabase super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get actionType => - $composableBuilder(column: $table.actionType, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get viewOffset => - $composableBuilder(column: $table.viewOffset, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get duration => - $composableBuilder(column: $table.duration, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get duration => $composableBuilder( + column: $table.duration, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get shouldMarkWatched => - $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get syncAttempts => - $composableBuilder(column: $table.syncAttempts, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get lastError => - $composableBuilder(column: $table.lastError, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnOrderings(column), + ); } -class $$OfflineWatchProgressTableAnnotationComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableAnnotationComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableAnnotationComposer({ required super.$db, required super.$table, @@ -2772,30 +3449,49 @@ class $$OfflineWatchProgressTableAnnotationComposer extends Composer<_$AppDataba super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get serverId => $composableBuilder(column: $table.serverId, builder: (column) => column); + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); - GeneratedColumn get ratingKey => $composableBuilder(column: $table.ratingKey, builder: (column) => column); + GeneratedColumn get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => column); - GeneratedColumn get globalKey => $composableBuilder(column: $table.globalKey, builder: (column) => column); + GeneratedColumn get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => column); - GeneratedColumn get actionType => $composableBuilder(column: $table.actionType, builder: (column) => column); + GeneratedColumn get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => column, + ); - GeneratedColumn get viewOffset => $composableBuilder(column: $table.viewOffset, builder: (column) => column); + GeneratedColumn get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => column, + ); - GeneratedColumn get duration => $composableBuilder(column: $table.duration, builder: (column) => column); + GeneratedColumn get duration => + $composableBuilder(column: $table.duration, builder: (column) => column); - GeneratedColumn get shouldMarkWatched => - $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => column); + GeneratedColumn get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => column, + ); - GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumn get syncAttempts => $composableBuilder(column: $table.syncAttempts, builder: (column) => column); + GeneratedColumn get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => column, + ); - GeneratedColumn get lastError => $composableBuilder(column: $table.lastError, builder: (column) => column); + GeneratedColumn get lastError => + $composableBuilder(column: $table.lastError, builder: (column) => column); } class $$OfflineWatchProgressTableTableManager @@ -2811,19 +3507,34 @@ class $$OfflineWatchProgressTableTableManager $$OfflineWatchProgressTableUpdateCompanionBuilder, ( OfflineWatchProgressItem, - BaseReferences<_$AppDatabase, $OfflineWatchProgressTable, OfflineWatchProgressItem>, + BaseReferences< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem + >, ), OfflineWatchProgressItem, PrefetchHooks Function() > { - $$OfflineWatchProgressTableTableManager(_$AppDatabase db, $OfflineWatchProgressTable table) - : super( + $$OfflineWatchProgressTableTableManager( + _$AppDatabase db, + $OfflineWatchProgressTable table, + ) : super( TableManagerState( db: db, table: table, - createFilteringComposer: () => $$OfflineWatchProgressTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$OfflineWatchProgressTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$OfflineWatchProgressTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$OfflineWatchProgressTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OfflineWatchProgressTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$OfflineWatchProgressTableAnnotationComposer( + $db: db, + $table: table, + ), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -2880,7 +3591,9 @@ class $$OfflineWatchProgressTableTableManager syncAttempts: syncAttempts, lastError: lastError, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2896,7 +3609,14 @@ typedef $$OfflineWatchProgressTableProcessedTableManager = $$OfflineWatchProgressTableAnnotationComposer, $$OfflineWatchProgressTableCreateCompanionBuilder, $$OfflineWatchProgressTableUpdateCompanionBuilder, - (OfflineWatchProgressItem, BaseReferences<_$AppDatabase, $OfflineWatchProgressTable, OfflineWatchProgressItem>), + ( + OfflineWatchProgressItem, + BaseReferences< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem + >, + ), OfflineWatchProgressItem, PrefetchHooks Function() >; @@ -2906,8 +3626,10 @@ class $AppDatabaseManager { $AppDatabaseManager(this._db); $$DownloadedMediaTableTableManager get downloadedMedia => $$DownloadedMediaTableTableManager(_db, _db.downloadedMedia); - $$DownloadQueueTableTableManager get downloadQueue => $$DownloadQueueTableTableManager(_db, _db.downloadQueue); - $$ApiCacheTableTableManager get apiCache => $$ApiCacheTableTableManager(_db, _db.apiCache); + $$DownloadQueueTableTableManager get downloadQueue => + $$DownloadQueueTableTableManager(_db, _db.downloadQueue); + $$ApiCacheTableTableManager get apiCache => + $$ApiCacheTableTableManager(_db, _db.apiCache); $$OfflineWatchProgressTableTableManager get offlineWatchProgress => $$OfflineWatchProgressTableTableManager(_db, _db.offlineWatchProgress); } diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 8445caca..40632af4 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 9 /// Strings: 5121 (569 per locale) /// -/// Built on 2026-02-08 at 11:03 UTC +/// Built on 2026-02-09 at 18:36 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/models/companion_remote/recent_remote_session.dart b/lib/models/companion_remote/recent_remote_session.dart new file mode 100644 index 00000000..b49f7213 --- /dev/null +++ b/lib/models/companion_remote/recent_remote_session.dart @@ -0,0 +1,44 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'recent_remote_session.g.dart'; + +/// Recent Companion Remote session for quick reconnection +@JsonSerializable() +class RecentRemoteSession { + final String sessionId; + final String pin; + final String deviceName; + final String platform; + final DateTime lastConnected; + + RecentRemoteSession({ + required this.sessionId, + required this.pin, + required this.deviceName, + required this.platform, + required this.lastConnected, + }); + + factory RecentRemoteSession.fromJson(Map json) => _$RecentRemoteSessionFromJson(json); + + Map toJson() => _$RecentRemoteSessionToJson(this); + + /// Create from QR code data (format: "sessionId:pin:deviceName:platform") + factory RecentRemoteSession.fromQrData(String qrData) { + final parts = qrData.split(':'); + if (parts.length < 2) { + throw FormatException('Invalid QR code format'); + } + + return RecentRemoteSession( + sessionId: parts[0], + pin: parts[1], + deviceName: parts.length > 2 ? parts[2] : 'Unknown Device', + platform: parts.length > 3 ? parts[3] : 'unknown', + lastConnected: DateTime.now(), + ); + } + + @override + String toString() => '$deviceName ($platform) - Last: ${lastConnected.toLocal()}'; +} diff --git a/lib/models/companion_remote/recent_remote_session.g.dart b/lib/models/companion_remote/recent_remote_session.g.dart new file mode 100644 index 00000000..6f5f0685 --- /dev/null +++ b/lib/models/companion_remote/recent_remote_session.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'recent_remote_session.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RecentRemoteSession _$RecentRemoteSessionFromJson(Map json) => RecentRemoteSession( + sessionId: json['sessionId'] as String, + pin: json['pin'] as String, + deviceName: json['deviceName'] as String, + platform: json['platform'] as String, + lastConnected: DateTime.parse(json['lastConnected'] as String), +); + +Map _$RecentRemoteSessionToJson(RecentRemoteSession instance) => { + 'sessionId': instance.sessionId, + 'pin': instance.pin, + 'deviceName': instance.deviceName, + 'platform': instance.platform, + 'lastConnected': instance.lastConnected.toIso8601String(), +}; diff --git a/lib/models/companion_remote/remote_command.dart b/lib/models/companion_remote/remote_command.dart index aab9879a..b9c16726 100644 --- a/lib/models/companion_remote/remote_command.dart +++ b/lib/models/companion_remote/remote_command.dart @@ -1,42 +1,24 @@ +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? data; - RemoteCommand({ - required this.type, - required this.deviceId, - required this.deviceName, - DateTime? timestamp, - this.data, - }) : timestamp = timestamp ?? DateTime.now(); + RemoteCommand({required this.type, required this.deviceId, required this.deviceName, DateTime? timestamp, this.data}) + : timestamp = timestamp ?? DateTime.now(); - Map toJson() { - return { - 'type': type.name, - 'deviceId': deviceId, - 'deviceName': deviceName, - 'timestamp': timestamp.toIso8601String(), - if (data != null) 'data': data, - }; - } + factory RemoteCommand.fromJson(Map json) => _$RemoteCommandFromJson(json); - factory RemoteCommand.fromJson(Map json) { - return RemoteCommand( - type: RemoteCommandType.values.firstWhere( - (e) => e.name == json['type'], - orElse: () => RemoteCommandType.ping, - ), - deviceId: json['deviceId'] as String, - deviceName: json['deviceName'] as String, - timestamp: DateTime.parse(json['timestamp'] as String), - data: json['data'] as Map?, - ); - } + Map toJson() => _$RemoteCommandToJson(this); RemoteCommand copyWith({ RemoteCommandType? type, @@ -72,9 +54,6 @@ class RemoteCommand { @override int get hashCode { - return type.hashCode ^ - deviceId.hashCode ^ - deviceName.hashCode ^ - timestamp.hashCode; + return type.hashCode ^ deviceId.hashCode ^ deviceName.hashCode ^ timestamp.hashCode; } } diff --git a/lib/models/companion_remote/remote_command.g.dart b/lib/models/companion_remote/remote_command.g.dart new file mode 100644 index 00000000..db7cab90 --- /dev/null +++ b/lib/models/companion_remote/remote_command.g.dart @@ -0,0 +1,67 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'remote_command.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RemoteCommand _$RemoteCommandFromJson(Map 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?, +); + +Map _$RemoteCommandToJson(RemoteCommand instance) => { + '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', +}; diff --git a/lib/models/companion_remote/remote_session.dart b/lib/models/companion_remote/remote_session.dart index 012e92df..a984ac7d 100644 --- a/lib/models/companion_remote/remote_session.dart +++ b/lib/models/companion_remote/remote_session.dart @@ -1,16 +1,12 @@ -enum RemoteSessionRole { - host, - remote, -} +import 'package:json_annotation/json_annotation.dart'; -enum RemoteSessionStatus { - disconnected, - connecting, - connected, - reconnecting, - error, -} +part 'remote_session.g.dart'; +enum RemoteSessionRole { host, remote } + +enum RemoteSessionStatus { disconnected, connecting, connected, reconnecting, error } + +@JsonSerializable() class RemoteDevice { final String id; final String name; @@ -24,28 +20,12 @@ class RemoteDevice { required this.platform, DateTime? connectedAt, Map? capabilities, - }) : connectedAt = connectedAt ?? DateTime.now(), - capabilities = capabilities ?? {}; + }) : connectedAt = connectedAt ?? DateTime.now(), + capabilities = capabilities ?? {}; - Map toJson() { - return { - 'id': id, - 'name': name, - 'platform': platform, - 'connectedAt': connectedAt.toIso8601String(), - 'capabilities': capabilities, - }; - } + factory RemoteDevice.fromJson(Map json) => _$RemoteDeviceFromJson(json); - factory RemoteDevice.fromJson(Map json) { - return RemoteDevice( - id: json['id'] as String, - name: json['name'] as String, - platform: json['platform'] as String, - connectedAt: DateTime.parse(json['connectedAt'] as String), - capabilities: Map.from(json['capabilities'] as Map? ?? {}), - ); - } + Map toJson() => _$RemoteDeviceToJson(this); RemoteDevice copyWith({ String? id, @@ -74,10 +54,13 @@ class RemoteDevice { int get hashCode => id.hashCode; } +@JsonSerializable() class RemoteSession { final String sessionId; final String pin; + @JsonKey(unknownEnumValue: RemoteSessionRole.remote) final RemoteSessionRole role; + @JsonKey(unknownEnumValue: RemoteSessionStatus.disconnected) final RemoteSessionStatus status; final RemoteDevice? connectedDevice; final DateTime createdAt; @@ -97,6 +80,10 @@ class RemoteSession { bool get isHost => role == RemoteSessionRole.host; bool get isRemote => role == RemoteSessionRole.remote; + factory RemoteSession.fromJson(Map json) => _$RemoteSessionFromJson(json); + + Map toJson() => _$RemoteSessionToJson(this); + RemoteSession copyWith({ String? sessionId, String? pin, @@ -116,36 +103,4 @@ class RemoteSession { errorMessage: errorMessage ?? this.errorMessage, ); } - - Map toJson() { - return { - 'sessionId': sessionId, - 'pin': pin, - 'role': role.name, - 'status': status.name, - 'connectedDevice': connectedDevice?.toJson(), - 'createdAt': createdAt.toIso8601String(), - 'errorMessage': errorMessage, - }; - } - - factory RemoteSession.fromJson(Map json) { - return RemoteSession( - sessionId: json['sessionId'] as String, - pin: json['pin'] as String, - role: RemoteSessionRole.values.firstWhere( - (e) => e.name == json['role'], - orElse: () => RemoteSessionRole.remote, - ), - status: RemoteSessionStatus.values.firstWhere( - (e) => e.name == json['status'], - orElse: () => RemoteSessionStatus.disconnected, - ), - connectedDevice: json['connectedDevice'] != null - ? RemoteDevice.fromJson(json['connectedDevice'] as Map) - : null, - createdAt: DateTime.parse(json['createdAt'] as String), - errorMessage: json['errorMessage'] as String?, - ); - } } diff --git a/lib/models/companion_remote/remote_session.g.dart b/lib/models/companion_remote/remote_session.g.dart new file mode 100644 index 00000000..00ae33c2 --- /dev/null +++ b/lib/models/companion_remote/remote_session.g.dart @@ -0,0 +1,61 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'remote_session.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RemoteDevice _$RemoteDeviceFromJson(Map json) => RemoteDevice( + id: json['id'] as String, + name: json['name'] as String, + platform: json['platform'] as String, + connectedAt: json['connectedAt'] == null ? null : DateTime.parse(json['connectedAt'] as String), + capabilities: (json['capabilities'] as Map?)?.map((k, e) => MapEntry(k, e as bool)), +); + +Map _$RemoteDeviceToJson(RemoteDevice instance) => { + 'id': instance.id, + 'name': instance.name, + 'platform': instance.platform, + 'connectedAt': instance.connectedAt.toIso8601String(), + 'capabilities': instance.capabilities, +}; + +RemoteSession _$RemoteSessionFromJson(Map json) => RemoteSession( + sessionId: json['sessionId'] as String, + pin: json['pin'] as String, + role: $enumDecode(_$RemoteSessionRoleEnumMap, json['role'], unknownValue: RemoteSessionRole.remote), + status: + $enumDecodeNullable( + _$RemoteSessionStatusEnumMap, + json['status'], + unknownValue: RemoteSessionStatus.disconnected, + ) ?? + RemoteSessionStatus.disconnected, + connectedDevice: json['connectedDevice'] == null + ? null + : RemoteDevice.fromJson(json['connectedDevice'] as Map), + createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String), + errorMessage: json['errorMessage'] as String?, +); + +Map _$RemoteSessionToJson(RemoteSession instance) => { + 'sessionId': instance.sessionId, + 'pin': instance.pin, + 'role': _$RemoteSessionRoleEnumMap[instance.role]!, + 'status': _$RemoteSessionStatusEnumMap[instance.status]!, + 'connectedDevice': instance.connectedDevice, + 'createdAt': instance.createdAt.toIso8601String(), + 'errorMessage': instance.errorMessage, +}; + +const _$RemoteSessionRoleEnumMap = {RemoteSessionRole.host: 'host', RemoteSessionRole.remote: 'remote'}; + +const _$RemoteSessionStatusEnumMap = { + RemoteSessionStatus.disconnected: 'disconnected', + RemoteSessionStatus.connecting: 'connecting', + RemoteSessionStatus.connected: 'connected', + RemoteSessionStatus.reconnecting: 'reconnecting', + RemoteSessionStatus.error: 'error', +}; diff --git a/lib/models/companion_remote/trusted_device.dart b/lib/models/companion_remote/trusted_device.dart index 35c8a9a3..e367c852 100644 --- a/lib/models/companion_remote/trusted_device.dart +++ b/lib/models/companion_remote/trusted_device.dart @@ -1,3 +1,8 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'trusted_device.g.dart'; + +@JsonSerializable() class TrustedDevice { final String peerId; final String deviceName; @@ -13,30 +18,12 @@ class TrustedDevice { DateTime? firstConnected, DateTime? lastConnected, this.isApproved = false, - }) : firstConnected = firstConnected ?? DateTime.now(), - lastConnected = lastConnected ?? DateTime.now(); + }) : firstConnected = firstConnected ?? DateTime.now(), + lastConnected = lastConnected ?? DateTime.now(); - Map toJson() { - return { - 'peerId': peerId, - 'deviceName': deviceName, - 'platform': platform, - 'firstConnected': firstConnected.toIso8601String(), - 'lastConnected': lastConnected.toIso8601String(), - 'isApproved': isApproved, - }; - } + factory TrustedDevice.fromJson(Map json) => _$TrustedDeviceFromJson(json); - factory TrustedDevice.fromJson(Map json) { - return TrustedDevice( - peerId: json['peerId'] as String, - deviceName: json['deviceName'] as String, - platform: json['platform'] as String, - firstConnected: DateTime.parse(json['firstConnected'] as String), - lastConnected: DateTime.parse(json['lastConnected'] as String), - isApproved: json['isApproved'] as bool? ?? false, - ); - } + Map toJson() => _$TrustedDeviceToJson(this); TrustedDevice copyWith({ String? peerId, diff --git a/lib/models/companion_remote/trusted_device.g.dart b/lib/models/companion_remote/trusted_device.g.dart new file mode 100644 index 00000000..38a195dc --- /dev/null +++ b/lib/models/companion_remote/trusted_device.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'trusted_device.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TrustedDevice _$TrustedDeviceFromJson(Map json) => TrustedDevice( + peerId: json['peerId'] as String, + deviceName: json['deviceName'] as String, + platform: json['platform'] as String, + firstConnected: json['firstConnected'] == null ? null : DateTime.parse(json['firstConnected'] as String), + lastConnected: json['lastConnected'] == null ? null : DateTime.parse(json['lastConnected'] as String), + isApproved: json['isApproved'] as bool? ?? false, +); + +Map _$TrustedDeviceToJson(TrustedDevice instance) => { + 'peerId': instance.peerId, + 'deviceName': instance.deviceName, + 'platform': instance.platform, + 'firstConnected': instance.firstConnected.toIso8601String(), + 'lastConnected': instance.lastConnected.toIso8601String(), + 'isApproved': instance.isApproved, +}; diff --git a/lib/models/play_queue_response.g.dart b/lib/models/play_queue_response.g.dart index 7a197697..8f8e3aa9 100644 --- a/lib/models/play_queue_response.g.dart +++ b/lib/models/play_queue_response.g.dart @@ -6,15 +6,23 @@ part of 'play_queue_response.dart'; // JsonSerializableGenerator // ************************************************************************** -PlayQueueResponse _$PlayQueueResponseFromJson(Map json) => PlayQueueResponse( - playQueueID: (json['playQueueID'] as num).toInt(), - playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?)?.toInt(), - playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?)?.toInt(), - playQueueSelectedMetadataItemID: json['playQueueSelectedMetadataItemID'] as String?, - playQueueShuffled: const BoolOrIntConverter().fromJson(json['playQueueShuffled'] as Object), - 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?)?.map((e) => PlexMetadata.fromJson(e as Map)).toList(), -); +PlayQueueResponse _$PlayQueueResponseFromJson(Map json) => + PlayQueueResponse( + playQueueID: (json['playQueueID'] as num).toInt(), + playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?) + ?.toInt(), + playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?) + ?.toInt(), + playQueueSelectedMetadataItemID: + json['playQueueSelectedMetadataItemID'] as String?, + playQueueShuffled: const BoolOrIntConverter().fromJson( + json['playQueueShuffled'] as Object, + ), + 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?) + ?.map((e) => PlexMetadata.fromJson(e as Map)) + .toList(), + ); diff --git a/lib/models/plex_library.g.dart b/lib/models/plex_library.g.dart index cbca367d..78565719 100644 --- a/lib/models/plex_library.g.dart +++ b/lib/models/plex_library.g.dart @@ -19,15 +19,16 @@ PlexLibrary _$PlexLibraryFromJson(Map json) => PlexLibrary( hidden: (json['hidden'] as num?)?.toInt(), ); -Map _$PlexLibraryToJson(PlexLibrary instance) => { - 'key': instance.key, - 'title': instance.title, - 'type': instance.type, - 'agent': instance.agent, - 'scanner': instance.scanner, - 'language': instance.language, - 'uuid': instance.uuid, - 'updatedAt': instance.updatedAt, - 'createdAt': instance.createdAt, - 'hidden': instance.hidden, -}; +Map _$PlexLibraryToJson(PlexLibrary instance) => + { + 'key': instance.key, + 'title': instance.title, + 'type': instance.type, + 'agent': instance.agent, + 'scanner': instance.scanner, + 'language': instance.language, + 'uuid': instance.uuid, + 'updatedAt': instance.updatedAt, + 'createdAt': instance.createdAt, + 'hidden': instance.hidden, + }; diff --git a/lib/models/plex_metadata.g.dart b/lib/models/plex_metadata.g.dart index d67a5fe4..adf96e91 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -41,7 +41,9 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( leafCount: (json['leafCount'] as num?)?.toInt(), viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(), childCount: (json['childCount'] as num?)?.toInt(), - role: (json['Role'] as List?)?.map((e) => PlexRole.fromJson(e as Map)).toList(), + role: (json['Role'] as List?) + ?.map((e) => PlexRole.fromJson(e as Map)) + .toList(), audioLanguage: json['audioLanguage'] as String?, subtitleLanguage: json['subtitleLanguage'] as String?, playlistItemID: (json['playlistItemID'] as num?)?.toInt(), @@ -52,48 +54,49 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( clearLogo: json['clearLogo'] as String?, ); -Map _$PlexMetadataToJson(PlexMetadata instance) => { - 'ratingKey': instance.ratingKey, - 'key': instance.key, - 'guid': instance.guid, - 'studio': instance.studio, - 'type': instance.type, - 'title': instance.title, - 'titleSort': instance.titleSort, - 'contentRating': instance.contentRating, - 'summary': instance.summary, - 'rating': instance.rating, - 'audienceRating': instance.audienceRating, - 'year': instance.year, - 'originallyAvailableAt': instance.originallyAvailableAt, - 'thumb': instance.thumb, - 'art': instance.art, - 'duration': instance.duration, - 'addedAt': instance.addedAt, - 'updatedAt': instance.updatedAt, - 'lastViewedAt': instance.lastViewedAt, - 'grandparentTitle': instance.grandparentTitle, - 'grandparentThumb': instance.grandparentThumb, - 'grandparentArt': instance.grandparentArt, - 'grandparentRatingKey': instance.grandparentRatingKey, - 'parentTitle': instance.parentTitle, - 'parentThumb': instance.parentThumb, - 'parentRatingKey': instance.parentRatingKey, - 'parentIndex': instance.parentIndex, - 'index': instance.index, - 'grandparentTheme': instance.grandparentTheme, - 'viewOffset': instance.viewOffset, - 'viewCount': instance.viewCount, - 'leafCount': instance.leafCount, - 'viewedLeafCount': instance.viewedLeafCount, - 'childCount': instance.childCount, - 'Role': instance.role, - 'audioLanguage': instance.audioLanguage, - 'subtitleLanguage': instance.subtitleLanguage, - 'playlistItemID': instance.playlistItemID, - 'playQueueItemID': instance.playQueueItemID, - 'librarySectionID': instance.librarySectionID, - 'ratingImage': instance.ratingImage, - 'audienceRatingImage': instance.audienceRatingImage, - 'clearLogo': instance.clearLogo, -}; +Map _$PlexMetadataToJson(PlexMetadata instance) => + { + 'ratingKey': instance.ratingKey, + 'key': instance.key, + 'guid': instance.guid, + 'studio': instance.studio, + 'type': instance.type, + 'title': instance.title, + 'titleSort': instance.titleSort, + 'contentRating': instance.contentRating, + 'summary': instance.summary, + 'rating': instance.rating, + 'audienceRating': instance.audienceRating, + 'year': instance.year, + 'originallyAvailableAt': instance.originallyAvailableAt, + 'thumb': instance.thumb, + 'art': instance.art, + 'duration': instance.duration, + 'addedAt': instance.addedAt, + 'updatedAt': instance.updatedAt, + 'lastViewedAt': instance.lastViewedAt, + 'grandparentTitle': instance.grandparentTitle, + 'grandparentThumb': instance.grandparentThumb, + 'grandparentArt': instance.grandparentArt, + 'grandparentRatingKey': instance.grandparentRatingKey, + 'parentTitle': instance.parentTitle, + 'parentThumb': instance.parentThumb, + 'parentRatingKey': instance.parentRatingKey, + 'parentIndex': instance.parentIndex, + 'index': instance.index, + 'grandparentTheme': instance.grandparentTheme, + 'viewOffset': instance.viewOffset, + 'viewCount': instance.viewCount, + 'leafCount': instance.leafCount, + 'viewedLeafCount': instance.viewedLeafCount, + 'childCount': instance.childCount, + 'Role': instance.role, + 'audioLanguage': instance.audioLanguage, + 'subtitleLanguage': instance.subtitleLanguage, + 'playlistItemID': instance.playlistItemID, + 'playQueueItemID': instance.playQueueItemID, + 'librarySectionID': instance.librarySectionID, + 'ratingImage': instance.ratingImage, + 'audienceRatingImage': instance.audienceRatingImage, + 'clearLogo': instance.clearLogo, + }; diff --git a/lib/models/plex_playlist.g.dart b/lib/models/plex_playlist.g.dart index a21f8f84..96647865 100644 --- a/lib/models/plex_playlist.g.dart +++ b/lib/models/plex_playlist.g.dart @@ -26,22 +26,23 @@ PlexPlaylist _$PlexPlaylistFromJson(Map json) => PlexPlaylist( thumb: json['thumb'] as String?, ); -Map _$PlexPlaylistToJson(PlexPlaylist instance) => { - 'ratingKey': instance.ratingKey, - 'key': instance.key, - 'type': instance.type, - 'title': instance.title, - 'summary': instance.summary, - 'smart': instance.smart, - 'playlistType': instance.playlistType, - 'duration': instance.duration, - 'leafCount': instance.leafCount, - 'composite': instance.composite, - 'addedAt': instance.addedAt, - 'updatedAt': instance.updatedAt, - 'lastViewedAt': instance.lastViewedAt, - 'viewCount': instance.viewCount, - 'content': instance.content, - 'guid': instance.guid, - 'thumb': instance.thumb, -}; +Map _$PlexPlaylistToJson(PlexPlaylist instance) => + { + 'ratingKey': instance.ratingKey, + 'key': instance.key, + 'type': instance.type, + 'title': instance.title, + 'summary': instance.summary, + 'smart': instance.smart, + 'playlistType': instance.playlistType, + 'duration': instance.duration, + 'leafCount': instance.leafCount, + 'composite': instance.composite, + 'addedAt': instance.addedAt, + 'updatedAt': instance.updatedAt, + 'lastViewedAt': instance.lastViewedAt, + 'viewCount': instance.viewCount, + 'content': instance.content, + 'guid': instance.guid, + 'thumb': instance.thumb, + }; diff --git a/lib/providers/companion_remote_provider.dart b/lib/providers/companion_remote_provider.dart index 98c4b4dd..f44935f3 100644 --- a/lib/providers/companion_remote_provider.dart +++ b/lib/providers/companion_remote_provider.dart @@ -10,6 +10,7 @@ import '../models/companion_remote/remote_command_type.dart'; import '../models/companion_remote/remote_session.dart'; import '../models/companion_remote/trusted_device.dart'; import '../services/companion_remote/companion_remote_peer_service.dart'; +import '../models/companion_remote/recent_remote_session.dart'; import '../services/companion_remote/companion_remote_discovery_service.dart'; import '../services/storage_service.dart'; import '../utils/app_logger.dart'; @@ -99,28 +100,28 @@ class CompanionRemoteProvider with ChangeNotifier { } void _setupPeerServiceListeners() { - _commandSubscription = _peerService!.onCommandReceived.listen((command) { - appLogger.d('CompanionRemote: Command received: ${command.type}'); + _commandSubscription = _peerService!.onCommandReceived.listen( + (command) { + appLogger.d('CompanionRemote: Command received: ${command.type}'); - if (command.type == RemoteCommandType.deviceInfo) { - _handleDeviceInfo(command); - } else if (command.type == RemoteCommandType.ping || - command.type == RemoteCommandType.pong || - command.type == RemoteCommandType.ack) { - // Don't call callback for these - } else { - onCommandReceived?.call(command); - } - }, onError: (error) { - appLogger.e('CompanionRemote: Stream error', error: error); - }); + if (command.type == RemoteCommandType.deviceInfo) { + _handleDeviceInfo(command); + } else if (command.type == RemoteCommandType.ping || + command.type == RemoteCommandType.pong || + command.type == RemoteCommandType.ack) { + // Don't call callback for these + } else { + onCommandReceived?.call(command); + } + }, + onError: (error) { + appLogger.e('CompanionRemote: Stream error', error: error); + }, + ); _deviceConnectedSubscription = _peerService!.onDeviceConnected.listen((device) async { appLogger.d('CompanionRemote: Device connected: ${device.name}'); - _session = _session?.copyWith( - status: RemoteSessionStatus.connected, - connectedDevice: device, - ); + _session = _session?.copyWith(status: RemoteSessionStatus.connected, connectedDevice: device); notifyListeners(); await addTrustedDevice(device, requireApproval: isHost); @@ -129,15 +130,10 @@ class CompanionRemoteProvider with ChangeNotifier { _deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) { appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)'); if (_intentionalDisconnect) { - _session = _session?.copyWith( - status: RemoteSessionStatus.disconnected, - connectedDevice: null, - ); + _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, connectedDevice: null); notifyListeners(); } else { - _session = _session?.copyWith( - status: RemoteSessionStatus.reconnecting, - ); + _session = _session?.copyWith(status: RemoteSessionStatus.reconnecting); notifyListeners(); _scheduleReconnect(); } @@ -145,10 +141,7 @@ class CompanionRemoteProvider with ChangeNotifier { _errorSubscription = _peerService!.onError.listen((error) { appLogger.e('CompanionRemote: Error: ${error.message}'); - _session = _session?.copyWith( - status: RemoteSessionStatus.error, - errorMessage: error.message, - ); + _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: error.message); notifyListeners(); }); @@ -166,11 +159,7 @@ class CompanionRemoteProvider with ChangeNotifier { appLogger.d('CompanionRemote: Device info - name: ${command.deviceName}, platform: $platform, role: $role'); - final device = RemoteDevice( - id: command.deviceId, - name: command.deviceName, - platform: platform, - ); + final device = RemoteDevice(id: command.deviceId, name: command.deviceName, platform: platform); _session = _session?.copyWith(connectedDevice: device); notifyListeners(); @@ -256,10 +245,7 @@ class CompanionRemoteProvider with ChangeNotifier { appLogger.d('CompanionRemote: Successfully joined session'); } catch (e) { appLogger.e('CompanionRemote: Failed to join session', error: e); - _session = _session?.copyWith( - status: RemoteSessionStatus.error, - errorMessage: e.toString(), - ); + _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString()); notifyListeners(); rethrow; } @@ -305,10 +291,7 @@ class CompanionRemoteProvider with ChangeNotifier { Future _attemptReconnect() async { if (_lastSessionId == null || _lastPin == null) { 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(); return; } @@ -347,10 +330,7 @@ class CompanionRemoteProvider with ChangeNotifier { void cancelReconnect() { _reconnectTimer?.cancel(); _reconnectAttempts = 0; - _session = _session?.copyWith( - status: RemoteSessionStatus.disconnected, - connectedDevice: null, - ); + _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, connectedDevice: null); notifyListeners(); } @@ -379,9 +359,7 @@ class CompanionRemoteProvider with ChangeNotifier { if (json != null) { final List list = jsonDecode(json); _trustedDevices.clear(); - _trustedDevices.addAll( - list.map((e) => TrustedDevice.fromJson(e as Map)), - ); + _trustedDevices.addAll(list.map((e) => TrustedDevice.fromJson(e as Map))); appLogger.d('CompanionRemote: Loaded ${_trustedDevices.length} trusted devices'); } } catch (e) { @@ -424,12 +402,7 @@ class CompanionRemoteProvider with ChangeNotifier { } _trustedDevices.add( - TrustedDevice( - peerId: device.id, - deviceName: device.name, - platform: device.platform, - isApproved: approved, - ), + TrustedDevice(peerId: device.id, deviceName: device.name, platform: device.platform, isApproved: approved), ); } diff --git a/lib/screens/companion_remote/pairing_screen.dart b/lib/screens/companion_remote/pairing_screen.dart index ca4d92ae..96bff012 100644 --- a/lib/screens/companion_remote/pairing_screen.dart +++ b/lib/screens/companion_remote/pairing_screen.dart @@ -6,7 +6,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:provider/provider.dart'; import '../../providers/companion_remote_provider.dart'; -import '../../services/companion_remote/companion_remote_discovery_service.dart'; +import '../../models/companion_remote/recent_remote_session.dart'; import '../../utils/app_logger.dart'; class PairingScreen extends StatefulWidget { @@ -105,10 +105,7 @@ class _PairingScreenState extends State { try { final provider = context.read(); - await provider.joinSession( - _sessionIdController.text.trim().toUpperCase(), - _pinController.text.trim(), - ); + await provider.joinSession(_sessionIdController.text.trim().toUpperCase(), _pinController.text.trim()); if (mounted) { Navigator.of(context).pop(); @@ -198,22 +195,10 @@ class _PairingScreenState extends State { children: [ SegmentedButton( segments: [ - const ButtonSegment( - value: 0, - label: Text('Recent'), - icon: Icon(Icons.history), - ), + const ButtonSegment(value: 0, label: Text('Recent'), icon: Icon(Icons.history)), if (_isMobile) - ButtonSegment( - value: _scanTabIndex, - label: const Text('Scan'), - icon: const Icon(Icons.qr_code_scanner), - ), - ButtonSegment( - value: _manualTabIndex, - label: const Text('Manual'), - icon: const Icon(Icons.keyboard), - ), + ButtonSegment(value: _scanTabIndex, label: const Text('Scan'), icon: const Icon(Icons.qr_code_scanner)), + ButtonSegment(value: _manualTabIndex, label: const Text('Manual'), icon: const Icon(Icons.keyboard)), ], selected: {_selectedTab}, onSelectionChanged: (Set selection) { @@ -222,9 +207,7 @@ class _PairingScreenState extends State { }); }, ), - Expanded( - child: _buildTabContent(), - ), + Expanded(child: _buildTabContent()), ], ), ); @@ -344,27 +327,16 @@ class _PairingScreenState extends State { if (_isDiscovering) ...[ const Center(child: CircularProgressIndicator()), const SizedBox(height: 16), - Text( - 'Loading...', - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), + Text('Loading...', style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center), ] else if (sessions.isEmpty) ...[ Card( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( children: [ - Icon( - Icons.devices_other, - size: 48, - color: Theme.of(context).colorScheme.outline, - ), + Icon(Icons.devices_other, size: 48, color: Theme.of(context).colorScheme.outline), const SizedBox(height: 16), - Text( - 'No recent connections', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('No recent connections', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Text( 'Connect to a device using Manual entry to get started', @@ -390,11 +362,7 @@ class _PairingScreenState extends State { ), isThreeLine: true, trailing: isThisConnecting - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ) + ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.arrow_forward), onTap: _isConnecting ? null : () => _connectToRecentSession(session), onLongPress: () => _showRemoveSessionDialog(session), @@ -410,17 +378,12 @@ class _PairingScreenState extends State { padding: const EdgeInsets.all(16.0), child: Row( children: [ - Icon( - Icons.error_outline, - color: Theme.of(context).colorScheme.onErrorContainer, - ), + Icon(Icons.error_outline, color: Theme.of(context).colorScheme.onErrorContainer), const SizedBox(width: 12), Expanded( child: Text( _errorMessage!, - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), + style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer), ), ), ], @@ -459,14 +422,8 @@ class _PairingScreenState extends State { title: const Text('Remove Recent Connection'), content: Text('Remove "${session.deviceName}" from recent connections?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Remove'), - ), + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Remove')), ], ), ); @@ -486,11 +443,7 @@ class _PairingScreenState extends State { children: [ const Icon(Icons.keyboard, size: 64, color: Colors.blue), const SizedBox(height: 24), - Text( - 'Pair with Desktop', - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ), + Text('Pair with Desktop', style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center), const SizedBox(height: 8), Text( 'Enter the session details shown on your desktop device', @@ -505,17 +458,12 @@ class _PairingScreenState extends State { padding: const EdgeInsets.all(16.0), child: Row( children: [ - Icon( - Icons.error_outline, - color: Theme.of(context).colorScheme.onErrorContainer, - ), + Icon(Icons.error_outline, color: Theme.of(context).colorScheme.onErrorContainer), const SizedBox(width: 12), Expanded( child: Text( _errorMessage!, - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), + style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer), ), ), ], @@ -571,10 +519,7 @@ class _PairingScreenState extends State { ), ), keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(6), - ], + inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)], validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a PIN'; @@ -590,21 +535,14 @@ class _PairingScreenState extends State { FilledButton.icon( onPressed: _isConnecting ? null : _connect, icon: _isConnecting - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) + ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.link), label: Text(_isConnecting ? 'Connecting...' : 'Connect'), ), const SizedBox(height: 32), const Divider(), const SizedBox(height: 16), - Text( - 'Tips', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('Tips', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), _buildTipCard( context, @@ -620,15 +558,11 @@ class _PairingScreenState extends State { ), ], const SizedBox(height: 8), - _buildTipCard( - context, - Icons.wifi, - 'Make sure both devices are on the same WiFi network', - ), - ], - ), + _buildTipCard(context, Icons.wifi, 'Make sure both devices are on the same WiFi network'), + ], ), - ); + ), + ); } Widget _buildTipCard(BuildContext context, IconData icon, String text) { @@ -639,12 +573,7 @@ class _PairingScreenState extends State { children: [ Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 12), - Expanded( - child: Text( - text, - style: Theme.of(context).textTheme.bodySmall, - ), - ), + Expanded(child: Text(text, style: Theme.of(context).textTheme.bodySmall)), ], ), ), diff --git a/lib/services/companion_remote/companion_remote_discovery_service.dart b/lib/services/companion_remote/companion_remote_discovery_service.dart index cb3bc716..f5136df9 100644 --- a/lib/services/companion_remote/companion_remote_discovery_service.dart +++ b/lib/services/companion_remote/companion_remote_discovery_service.dart @@ -1,65 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import '../../models/companion_remote/recent_remote_session.dart'; import '../../services/storage_service.dart'; import '../../utils/app_logger.dart'; -/// Recent Companion Remote session for quick reconnection -class RecentRemoteSession { - final String sessionId; - final String pin; - final String deviceName; - final String platform; - final DateTime lastConnected; - - RecentRemoteSession({ - required this.sessionId, - required this.pin, - required this.deviceName, - required this.platform, - required this.lastConnected, - }); - - factory RecentRemoteSession.fromJson(Map json) { - return RecentRemoteSession( - sessionId: json['sessionId'] as String, - pin: json['pin'] as String, - deviceName: json['deviceName'] as String, - platform: json['platform'] as String, - lastConnected: DateTime.parse(json['lastConnected'] as String), - ); - } - - Map toJson() { - return { - 'sessionId': sessionId, - 'pin': pin, - 'deviceName': deviceName, - 'platform': platform, - 'lastConnected': lastConnected.toIso8601String(), - }; - } - - /// Create from QR code data (format: "sessionId:pin:deviceName:platform") - factory RecentRemoteSession.fromQrData(String qrData) { - final parts = qrData.split(':'); - if (parts.length < 2) { - throw FormatException('Invalid QR code format'); - } - - return RecentRemoteSession( - sessionId: parts[0], - pin: parts[1], - deviceName: parts.length > 2 ? parts[2] : 'Unknown Device', - platform: parts.length > 3 ? parts[3] : 'unknown', - lastConnected: DateTime.now(), - ); - } - - @override - String toString() => '$deviceName ($platform) - Last: ${lastConnected.toLocal()}'; -} - /// Service for managing recent Companion Remote sessions class CompanionRemoteDiscoveryService { static const String _storageKey = 'companion_remote_recent_sessions'; @@ -87,9 +32,7 @@ class CompanionRemoteDiscoveryService { if (json != null) { final List list = jsonDecode(json); _recentSessions.clear(); - _recentSessions.addAll( - list.map((e) => RecentRemoteSession.fromJson(e as Map)), - ); + _recentSessions.addAll(list.map((e) => RecentRemoteSession.fromJson(e as Map))); // Sort by last connected (most recent first) _recentSessions.sort((a, b) => b.lastConnected.compareTo(a.lastConnected)); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index b2650411..db8f2cab 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -60,7 +60,7 @@ PODS: - wakelock_plus (0.0.1): - FlutterMacOS - WebRTC-SDK (137.7151.04) - - window_manager (0.2.0): + - window_manager (0.5.0): - FlutterMacOS DEPENDENCIES: @@ -146,7 +146,7 @@ SPEC CHECKSUMS: url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + window_manager: b729e31d38fb04905235df9ea896128991cad99e PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d From e30c0b437077349c310e9eda2bdaf38324aa0e51 Mon Sep 17 00:00:00 2001 From: Matt Vogel Date: Mon, 9 Feb 2026 18:45:10 -0500 Subject: [PATCH 4/8] Migrate companion remote to WebSocket --- lib/database/app_database.g.dart | 1522 +++++------------ .../recent_remote_session.dart | 24 +- .../recent_remote_session.g.dart | 2 + .../companion_remote/remote_session.dart | 6 +- lib/models/play_queue_response.g.dart | 32 +- lib/models/plex_library.g.dart | 25 +- lib/models/plex_metadata.g.dart | 95 +- lib/models/plex_playlist.g.dart | 39 +- lib/providers/companion_remote_provider.dart | 37 +- .../mobile_remote_screen.dart | 140 +- .../companion_remote/pairing_screen.dart | 54 +- lib/screens/discover_screen.dart | 16 +- lib/screens/settings/settings_screen.dart | 5 +- .../companion_remote_peer_service.dart | 617 ++++--- lib/services/gamepad_service.dart | 1 - .../remote_session_dialog.dart | 143 +- linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - macos/Podfile.lock | 10 - pubspec.lock | 40 - pubspec.yaml | 4 - .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 24 files changed, 1035 insertions(+), 1788 deletions(-) diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index db269630..6844c0e6 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -3,8 +3,7 @@ part of 'app_database.dart'; // ignore_for_file: type=lint -class $DownloadedMediaTable extends DownloadedMedia - with TableInfo<$DownloadedMediaTable, DownloadedMediaItem> { +class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMediaTable, DownloadedMediaItem> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -18,13 +17,9 @@ class $DownloadedMediaTable extends DownloadedMedia hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'PRIMARY KEY AUTOINCREMENT', - ), - ); - static const VerificationMeta _serverIdMeta = const VerificationMeta( - 'serverId', + defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), ); + static const VerificationMeta _serverIdMeta = const VerificationMeta('serverId'); @override late final GeneratedColumn serverId = GeneratedColumn( 'server_id', @@ -33,9 +28,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _ratingKeyMeta = const VerificationMeta( - 'ratingKey', - ); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta('ratingKey'); @override late final GeneratedColumn ratingKey = GeneratedColumn( 'rating_key', @@ -44,9 +37,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _globalKeyMeta = const VerificationMeta( - 'globalKey', - ); + static const VerificationMeta _globalKeyMeta = const VerificationMeta('globalKey'); @override late final GeneratedColumn globalKey = GeneratedColumn( 'global_key', @@ -65,9 +56,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _parentRatingKeyMeta = const VerificationMeta( - 'parentRatingKey', - ); + static const VerificationMeta _parentRatingKeyMeta = const VerificationMeta('parentRatingKey'); @override late final GeneratedColumn parentRatingKey = GeneratedColumn( 'parent_rating_key', @@ -76,17 +65,15 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _grandparentRatingKeyMeta = - const VerificationMeta('grandparentRatingKey'); + static const VerificationMeta _grandparentRatingKeyMeta = const VerificationMeta('grandparentRatingKey'); @override - late final GeneratedColumn grandparentRatingKey = - GeneratedColumn( - 'grandparent_rating_key', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); + late final GeneratedColumn grandparentRatingKey = GeneratedColumn( + 'grandparent_rating_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); static const VerificationMeta _statusMeta = const VerificationMeta('status'); @override late final GeneratedColumn status = GeneratedColumn( @@ -96,9 +83,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _progressMeta = const VerificationMeta( - 'progress', - ); + static const VerificationMeta _progressMeta = const VerificationMeta('progress'); @override late final GeneratedColumn progress = GeneratedColumn( 'progress', @@ -108,9 +93,7 @@ class $DownloadedMediaTable extends DownloadedMedia requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _totalBytesMeta = const VerificationMeta( - 'totalBytes', - ); + static const VerificationMeta _totalBytesMeta = const VerificationMeta('totalBytes'); @override late final GeneratedColumn totalBytes = GeneratedColumn( 'total_bytes', @@ -119,9 +102,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _downloadedBytesMeta = const VerificationMeta( - 'downloadedBytes', - ); + static const VerificationMeta _downloadedBytesMeta = const VerificationMeta('downloadedBytes'); @override late final GeneratedColumn downloadedBytes = GeneratedColumn( 'downloaded_bytes', @@ -131,9 +112,7 @@ class $DownloadedMediaTable extends DownloadedMedia requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _videoFilePathMeta = const VerificationMeta( - 'videoFilePath', - ); + static const VerificationMeta _videoFilePathMeta = const VerificationMeta('videoFilePath'); @override late final GeneratedColumn videoFilePath = GeneratedColumn( 'video_file_path', @@ -142,9 +121,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _thumbPathMeta = const VerificationMeta( - 'thumbPath', - ); + static const VerificationMeta _thumbPathMeta = const VerificationMeta('thumbPath'); @override late final GeneratedColumn thumbPath = GeneratedColumn( 'thumb_path', @@ -153,9 +130,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _downloadedAtMeta = const VerificationMeta( - 'downloadedAt', - ); + static const VerificationMeta _downloadedAtMeta = const VerificationMeta('downloadedAt'); @override late final GeneratedColumn downloadedAt = GeneratedColumn( 'downloaded_at', @@ -164,9 +139,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _errorMessageMeta = const VerificationMeta( - 'errorMessage', - ); + static const VerificationMeta _errorMessageMeta = const VerificationMeta('errorMessage'); @override late final GeneratedColumn errorMessage = GeneratedColumn( 'error_message', @@ -175,9 +148,7 @@ class $DownloadedMediaTable extends DownloadedMedia type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _retryCountMeta = const VerificationMeta( - 'retryCount', - ); + static const VerificationMeta _retryCountMeta = const VerificationMeta('retryCount'); @override late final GeneratedColumn retryCount = GeneratedColumn( 'retry_count', @@ -212,132 +183,78 @@ class $DownloadedMediaTable extends DownloadedMedia String get actualTableName => $name; static const String $name = 'downloaded_media'; @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } if (data.containsKey('server_id')) { - context.handle( - _serverIdMeta, - serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), - ); + context.handle(_serverIdMeta, serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta)); } else if (isInserting) { context.missing(_serverIdMeta); } if (data.containsKey('rating_key')) { - context.handle( - _ratingKeyMeta, - ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), - ); + context.handle(_ratingKeyMeta, ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta)); } else if (isInserting) { context.missing(_ratingKeyMeta); } if (data.containsKey('global_key')) { - context.handle( - _globalKeyMeta, - globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), - ); + context.handle(_globalKeyMeta, globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta)); } else if (isInserting) { context.missing(_globalKeyMeta); } if (data.containsKey('type')) { - context.handle( - _typeMeta, - type.isAcceptableOrUnknown(data['type']!, _typeMeta), - ); + context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); } else if (isInserting) { context.missing(_typeMeta); } if (data.containsKey('parent_rating_key')) { context.handle( _parentRatingKeyMeta, - parentRatingKey.isAcceptableOrUnknown( - data['parent_rating_key']!, - _parentRatingKeyMeta, - ), + parentRatingKey.isAcceptableOrUnknown(data['parent_rating_key']!, _parentRatingKeyMeta), ); } if (data.containsKey('grandparent_rating_key')) { context.handle( _grandparentRatingKeyMeta, - grandparentRatingKey.isAcceptableOrUnknown( - data['grandparent_rating_key']!, - _grandparentRatingKeyMeta, - ), + grandparentRatingKey.isAcceptableOrUnknown(data['grandparent_rating_key']!, _grandparentRatingKeyMeta), ); } if (data.containsKey('status')) { - context.handle( - _statusMeta, - status.isAcceptableOrUnknown(data['status']!, _statusMeta), - ); + context.handle(_statusMeta, status.isAcceptableOrUnknown(data['status']!, _statusMeta)); } else if (isInserting) { context.missing(_statusMeta); } if (data.containsKey('progress')) { - context.handle( - _progressMeta, - progress.isAcceptableOrUnknown(data['progress']!, _progressMeta), - ); + context.handle(_progressMeta, progress.isAcceptableOrUnknown(data['progress']!, _progressMeta)); } if (data.containsKey('total_bytes')) { - context.handle( - _totalBytesMeta, - totalBytes.isAcceptableOrUnknown(data['total_bytes']!, _totalBytesMeta), - ); + context.handle(_totalBytesMeta, totalBytes.isAcceptableOrUnknown(data['total_bytes']!, _totalBytesMeta)); } if (data.containsKey('downloaded_bytes')) { context.handle( _downloadedBytesMeta, - downloadedBytes.isAcceptableOrUnknown( - data['downloaded_bytes']!, - _downloadedBytesMeta, - ), + downloadedBytes.isAcceptableOrUnknown(data['downloaded_bytes']!, _downloadedBytesMeta), ); } if (data.containsKey('video_file_path')) { context.handle( _videoFilePathMeta, - videoFilePath.isAcceptableOrUnknown( - data['video_file_path']!, - _videoFilePathMeta, - ), + videoFilePath.isAcceptableOrUnknown(data['video_file_path']!, _videoFilePathMeta), ); } if (data.containsKey('thumb_path')) { - context.handle( - _thumbPathMeta, - thumbPath.isAcceptableOrUnknown(data['thumb_path']!, _thumbPathMeta), - ); + context.handle(_thumbPathMeta, thumbPath.isAcceptableOrUnknown(data['thumb_path']!, _thumbPathMeta)); } if (data.containsKey('downloaded_at')) { - context.handle( - _downloadedAtMeta, - downloadedAt.isAcceptableOrUnknown( - data['downloaded_at']!, - _downloadedAtMeta, - ), - ); + context.handle(_downloadedAtMeta, downloadedAt.isAcceptableOrUnknown(data['downloaded_at']!, _downloadedAtMeta)); } if (data.containsKey('error_message')) { - context.handle( - _errorMessageMeta, - errorMessage.isAcceptableOrUnknown( - data['error_message']!, - _errorMessageMeta, - ), - ); + context.handle(_errorMessageMeta, errorMessage.isAcceptableOrUnknown(data['error_message']!, _errorMessageMeta)); } if (data.containsKey('retry_count')) { - context.handle( - _retryCountMeta, - retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta), - ); + context.handle(_retryCountMeta, retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta)); } return context; } @@ -348,26 +265,11 @@ class $DownloadedMediaTable extends DownloadedMedia DownloadedMediaItem map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return DownloadedMediaItem( - id: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}id'], - )!, - serverId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}server_id'], - )!, - ratingKey: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}rating_key'], - )!, - globalKey: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}global_key'], - )!, - type: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}type'], - )!, + id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, + serverId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}server_id'])!, + ratingKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}rating_key'])!, + globalKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}global_key'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}type'])!, parentRatingKey: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}parent_rating_key'], @@ -376,42 +278,15 @@ class $DownloadedMediaTable extends DownloadedMedia DriftSqlType.string, data['${effectivePrefix}grandparent_rating_key'], ), - status: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}status'], - )!, - progress: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}progress'], - )!, - totalBytes: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}total_bytes'], - ), - downloadedBytes: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}downloaded_bytes'], - )!, - videoFilePath: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}video_file_path'], - ), - thumbPath: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}thumb_path'], - ), - downloadedAt: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}downloaded_at'], - ), - errorMessage: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}error_message'], - ), - retryCount: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}retry_count'], - )!, + status: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}status'])!, + progress: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}progress'])!, + totalBytes: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}total_bytes']), + downloadedBytes: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}downloaded_bytes'])!, + videoFilePath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}video_file_path']), + thumbPath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumb_path']), + downloadedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}downloaded_at']), + errorMessage: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}error_message']), + retryCount: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}retry_count'])!, ); } @@ -421,8 +296,7 @@ class $DownloadedMediaTable extends DownloadedMedia } } -class DownloadedMediaItem extends DataClass - implements Insertable { +class DownloadedMediaItem extends DataClass implements Insertable { final int id; final String serverId; final String ratingKey; @@ -500,38 +374,23 @@ class DownloadedMediaItem extends DataClass ratingKey: Value(ratingKey), globalKey: Value(globalKey), type: Value(type), - parentRatingKey: parentRatingKey == null && nullToAbsent - ? const Value.absent() - : Value(parentRatingKey), + parentRatingKey: parentRatingKey == null && nullToAbsent ? const Value.absent() : Value(parentRatingKey), grandparentRatingKey: grandparentRatingKey == null && nullToAbsent ? const Value.absent() : Value(grandparentRatingKey), status: Value(status), progress: Value(progress), - totalBytes: totalBytes == null && nullToAbsent - ? const Value.absent() - : Value(totalBytes), + totalBytes: totalBytes == null && nullToAbsent ? const Value.absent() : Value(totalBytes), downloadedBytes: Value(downloadedBytes), - videoFilePath: videoFilePath == null && nullToAbsent - ? const Value.absent() - : Value(videoFilePath), - thumbPath: thumbPath == null && nullToAbsent - ? const Value.absent() - : Value(thumbPath), - downloadedAt: downloadedAt == null && nullToAbsent - ? const Value.absent() - : Value(downloadedAt), - errorMessage: errorMessage == null && nullToAbsent - ? const Value.absent() - : Value(errorMessage), + videoFilePath: videoFilePath == null && nullToAbsent ? const Value.absent() : Value(videoFilePath), + thumbPath: thumbPath == null && nullToAbsent ? const Value.absent() : Value(thumbPath), + downloadedAt: downloadedAt == null && nullToAbsent ? const Value.absent() : Value(downloadedAt), + errorMessage: errorMessage == null && nullToAbsent ? const Value.absent() : Value(errorMessage), retryCount: Value(retryCount), ); } - factory DownloadedMediaItem.fromJson( - Map json, { - ValueSerializer? serializer, - }) { + factory DownloadedMediaItem.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return DownloadedMediaItem( id: serializer.fromJson(json['id']), @@ -540,9 +399,7 @@ class DownloadedMediaItem extends DataClass globalKey: serializer.fromJson(json['globalKey']), type: serializer.fromJson(json['type']), parentRatingKey: serializer.fromJson(json['parentRatingKey']), - grandparentRatingKey: serializer.fromJson( - json['grandparentRatingKey'], - ), + grandparentRatingKey: serializer.fromJson(json['grandparentRatingKey']), status: serializer.fromJson(json['status']), progress: serializer.fromJson(json['progress']), totalBytes: serializer.fromJson(json['totalBytes']), @@ -600,19 +457,13 @@ class DownloadedMediaItem extends DataClass ratingKey: ratingKey ?? this.ratingKey, globalKey: globalKey ?? this.globalKey, type: type ?? this.type, - parentRatingKey: parentRatingKey.present - ? parentRatingKey.value - : this.parentRatingKey, - grandparentRatingKey: grandparentRatingKey.present - ? grandparentRatingKey.value - : this.grandparentRatingKey, + parentRatingKey: parentRatingKey.present ? parentRatingKey.value : this.parentRatingKey, + grandparentRatingKey: grandparentRatingKey.present ? grandparentRatingKey.value : this.grandparentRatingKey, status: status ?? this.status, progress: progress ?? this.progress, totalBytes: totalBytes.present ? totalBytes.value : this.totalBytes, downloadedBytes: downloadedBytes ?? this.downloadedBytes, - videoFilePath: videoFilePath.present - ? videoFilePath.value - : this.videoFilePath, + videoFilePath: videoFilePath.present ? videoFilePath.value : this.videoFilePath, thumbPath: thumbPath.present ? thumbPath.value : this.thumbPath, downloadedAt: downloadedAt.present ? downloadedAt.value : this.downloadedAt, errorMessage: errorMessage.present ? errorMessage.value : this.errorMessage, @@ -625,33 +476,19 @@ class DownloadedMediaItem extends DataClass ratingKey: data.ratingKey.present ? data.ratingKey.value : this.ratingKey, globalKey: data.globalKey.present ? data.globalKey.value : this.globalKey, type: data.type.present ? data.type.value : this.type, - parentRatingKey: data.parentRatingKey.present - ? data.parentRatingKey.value - : this.parentRatingKey, + parentRatingKey: data.parentRatingKey.present ? data.parentRatingKey.value : this.parentRatingKey, grandparentRatingKey: data.grandparentRatingKey.present ? data.grandparentRatingKey.value : this.grandparentRatingKey, status: data.status.present ? data.status.value : this.status, progress: data.progress.present ? data.progress.value : this.progress, - totalBytes: data.totalBytes.present - ? data.totalBytes.value - : this.totalBytes, - downloadedBytes: data.downloadedBytes.present - ? data.downloadedBytes.value - : this.downloadedBytes, - videoFilePath: data.videoFilePath.present - ? data.videoFilePath.value - : this.videoFilePath, + totalBytes: data.totalBytes.present ? data.totalBytes.value : this.totalBytes, + downloadedBytes: data.downloadedBytes.present ? data.downloadedBytes.value : this.downloadedBytes, + videoFilePath: data.videoFilePath.present ? data.videoFilePath.value : this.videoFilePath, thumbPath: data.thumbPath.present ? data.thumbPath.value : this.thumbPath, - downloadedAt: data.downloadedAt.present - ? data.downloadedAt.value - : this.downloadedAt, - errorMessage: data.errorMessage.present - ? data.errorMessage.value - : this.errorMessage, - retryCount: data.retryCount.present - ? data.retryCount.value - : this.retryCount, + downloadedAt: data.downloadedAt.present ? data.downloadedAt.value : this.downloadedAt, + errorMessage: data.errorMessage.present ? data.errorMessage.value : this.errorMessage, + retryCount: data.retryCount.present ? data.retryCount.value : this.retryCount, ); } @@ -801,8 +638,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { if (globalKey != null) 'global_key': globalKey, if (type != null) 'type': type, if (parentRatingKey != null) 'parent_rating_key': parentRatingKey, - if (grandparentRatingKey != null) - 'grandparent_rating_key': grandparentRatingKey, + if (grandparentRatingKey != null) 'grandparent_rating_key': grandparentRatingKey, if (status != null) 'status': status, if (progress != null) 'progress': progress, if (totalBytes != null) 'total_bytes': totalBytes, @@ -875,9 +711,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { map['parent_rating_key'] = Variable(parentRatingKey.value); } if (grandparentRatingKey.present) { - map['grandparent_rating_key'] = Variable( - grandparentRatingKey.value, - ); + map['grandparent_rating_key'] = Variable(grandparentRatingKey.value); } if (status.present) { map['status'] = Variable(status.value); @@ -933,8 +767,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { } } -class $DownloadQueueTable extends DownloadQueue - with TableInfo<$DownloadQueueTable, DownloadQueueItem> { +class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTable, DownloadQueueItem> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -948,13 +781,9 @@ class $DownloadQueueTable extends DownloadQueue hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'PRIMARY KEY AUTOINCREMENT', - ), - ); - static const VerificationMeta _mediaGlobalKeyMeta = const VerificationMeta( - 'mediaGlobalKey', + defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), ); + static const VerificationMeta _mediaGlobalKeyMeta = const VerificationMeta('mediaGlobalKey'); @override late final GeneratedColumn mediaGlobalKey = GeneratedColumn( 'media_global_key', @@ -964,9 +793,7 @@ class $DownloadQueueTable extends DownloadQueue requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), ); - static const VerificationMeta _priorityMeta = const VerificationMeta( - 'priority', - ); + static const VerificationMeta _priorityMeta = const VerificationMeta('priority'); @override late final GeneratedColumn priority = GeneratedColumn( 'priority', @@ -976,9 +803,7 @@ class $DownloadQueueTable extends DownloadQueue requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _addedAtMeta = const VerificationMeta( - 'addedAt', - ); + static const VerificationMeta _addedAtMeta = const VerificationMeta('addedAt'); @override late final GeneratedColumn addedAt = GeneratedColumn( 'added_at', @@ -987,9 +812,7 @@ class $DownloadQueueTable extends DownloadQueue type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _downloadSubtitlesMeta = const VerificationMeta( - 'downloadSubtitles', - ); + static const VerificationMeta _downloadSubtitlesMeta = const VerificationMeta('downloadSubtitles'); @override late final GeneratedColumn downloadSubtitles = GeneratedColumn( 'download_subtitles', @@ -997,14 +820,10 @@ class $DownloadQueueTable extends DownloadQueue false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("download_subtitles" IN (0, 1))', - ), + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("download_subtitles" IN (0, 1))'), defaultValue: const Constant(true), ); - static const VerificationMeta _downloadArtworkMeta = const VerificationMeta( - 'downloadArtwork', - ); + static const VerificationMeta _downloadArtworkMeta = const VerificationMeta('downloadArtwork'); @override late final GeneratedColumn downloadArtwork = GeneratedColumn( 'download_artwork', @@ -1012,30 +831,18 @@ class $DownloadQueueTable extends DownloadQueue false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("download_artwork" IN (0, 1))', - ), + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("download_artwork" IN (0, 1))'), defaultValue: const Constant(true), ); @override - List get $columns => [ - id, - mediaGlobalKey, - priority, - addedAt, - downloadSubtitles, - downloadArtwork, - ]; + List get $columns => [id, mediaGlobalKey, priority, addedAt, downloadSubtitles, downloadArtwork]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'download_queue'; @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -1044,44 +851,29 @@ class $DownloadQueueTable extends DownloadQueue if (data.containsKey('media_global_key')) { context.handle( _mediaGlobalKeyMeta, - mediaGlobalKey.isAcceptableOrUnknown( - data['media_global_key']!, - _mediaGlobalKeyMeta, - ), + mediaGlobalKey.isAcceptableOrUnknown(data['media_global_key']!, _mediaGlobalKeyMeta), ); } else if (isInserting) { context.missing(_mediaGlobalKeyMeta); } if (data.containsKey('priority')) { - context.handle( - _priorityMeta, - priority.isAcceptableOrUnknown(data['priority']!, _priorityMeta), - ); + context.handle(_priorityMeta, priority.isAcceptableOrUnknown(data['priority']!, _priorityMeta)); } if (data.containsKey('added_at')) { - context.handle( - _addedAtMeta, - addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta), - ); + context.handle(_addedAtMeta, addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta)); } else if (isInserting) { context.missing(_addedAtMeta); } if (data.containsKey('download_subtitles')) { context.handle( _downloadSubtitlesMeta, - downloadSubtitles.isAcceptableOrUnknown( - data['download_subtitles']!, - _downloadSubtitlesMeta, - ), + downloadSubtitles.isAcceptableOrUnknown(data['download_subtitles']!, _downloadSubtitlesMeta), ); } if (data.containsKey('download_artwork')) { context.handle( _downloadArtworkMeta, - downloadArtwork.isAcceptableOrUnknown( - data['download_artwork']!, - _downloadArtworkMeta, - ), + downloadArtwork.isAcceptableOrUnknown(data['download_artwork']!, _downloadArtworkMeta), ); } return context; @@ -1093,22 +885,13 @@ class $DownloadQueueTable extends DownloadQueue DownloadQueueItem map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return DownloadQueueItem( - id: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}id'], - )!, + id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, mediaGlobalKey: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}media_global_key'], )!, - priority: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}priority'], - )!, - addedAt: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}added_at'], - )!, + priority: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}priority'])!, + addedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}added_at'])!, downloadSubtitles: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}download_subtitles'], @@ -1126,8 +909,7 @@ class $DownloadQueueTable extends DownloadQueue } } -class DownloadQueueItem extends DataClass - implements Insertable { +class DownloadQueueItem extends DataClass implements Insertable { final int id; final String mediaGlobalKey; final int priority; @@ -1165,10 +947,7 @@ class DownloadQueueItem extends DataClass ); } - factory DownloadQueueItem.fromJson( - Map json, { - ValueSerializer? serializer, - }) { + factory DownloadQueueItem.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return DownloadQueueItem( id: serializer.fromJson(json['id']), @@ -1210,17 +989,11 @@ class DownloadQueueItem extends DataClass DownloadQueueItem copyWithCompanion(DownloadQueueCompanion data) { return DownloadQueueItem( id: data.id.present ? data.id.value : this.id, - mediaGlobalKey: data.mediaGlobalKey.present - ? data.mediaGlobalKey.value - : this.mediaGlobalKey, + mediaGlobalKey: data.mediaGlobalKey.present ? data.mediaGlobalKey.value : this.mediaGlobalKey, priority: data.priority.present ? data.priority.value : this.priority, addedAt: data.addedAt.present ? data.addedAt.value : this.addedAt, - downloadSubtitles: data.downloadSubtitles.present - ? data.downloadSubtitles.value - : this.downloadSubtitles, - downloadArtwork: data.downloadArtwork.present - ? data.downloadArtwork.value - : this.downloadArtwork, + downloadSubtitles: data.downloadSubtitles.present ? data.downloadSubtitles.value : this.downloadSubtitles, + downloadArtwork: data.downloadArtwork.present ? data.downloadArtwork.value : this.downloadArtwork, ); } @@ -1238,14 +1011,7 @@ class DownloadQueueItem extends DataClass } @override - int get hashCode => Object.hash( - id, - mediaGlobalKey, - priority, - addedAt, - downloadSubtitles, - downloadArtwork, - ); + int get hashCode => Object.hash(id, mediaGlobalKey, priority, addedAt, downloadSubtitles, downloadArtwork); @override bool operator ==(Object other) => identical(this, other) || @@ -1356,15 +1122,12 @@ class DownloadQueueCompanion extends UpdateCompanion { } } -class $ApiCacheTable extends ApiCache - with TableInfo<$ApiCacheTable, ApiCacheData> { +class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; $ApiCacheTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _cacheKeyMeta = const VerificationMeta( - 'cacheKey', - ); + static const VerificationMeta _cacheKeyMeta = const VerificationMeta('cacheKey'); @override late final GeneratedColumn cacheKey = GeneratedColumn( 'cache_key', @@ -1390,14 +1153,10 @@ class $ApiCacheTable extends ApiCache false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("pinned" IN (0, 1))', - ), + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), defaultValue: const Constant(false), ); - static const VerificationMeta _cachedAtMeta = const VerificationMeta( - 'cachedAt', - ); + static const VerificationMeta _cachedAtMeta = const VerificationMeta('cachedAt'); @override late final GeneratedColumn cachedAt = GeneratedColumn( 'cached_at', @@ -1415,39 +1174,24 @@ class $ApiCacheTable extends ApiCache String get actualTableName => $name; static const String $name = 'api_cache'; @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('cache_key')) { - context.handle( - _cacheKeyMeta, - cacheKey.isAcceptableOrUnknown(data['cache_key']!, _cacheKeyMeta), - ); + context.handle(_cacheKeyMeta, cacheKey.isAcceptableOrUnknown(data['cache_key']!, _cacheKeyMeta)); } else if (isInserting) { context.missing(_cacheKeyMeta); } if (data.containsKey('data')) { - context.handle( - _dataMeta, - this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), - ); + context.handle(_dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); } else if (isInserting) { context.missing(_dataMeta); } if (data.containsKey('pinned')) { - context.handle( - _pinnedMeta, - pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta), - ); + context.handle(_pinnedMeta, pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); } if (data.containsKey('cached_at')) { - context.handle( - _cachedAtMeta, - cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta), - ); + context.handle(_cachedAtMeta, cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta)); } return context; } @@ -1458,22 +1202,10 @@ class $ApiCacheTable extends ApiCache ApiCacheData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ApiCacheData( - cacheKey: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}cache_key'], - )!, - data: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}data'], - )!, - pinned: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}pinned'], - )!, - cachedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, - data['${effectivePrefix}cached_at'], - )!, + cacheKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}cache_key'])!, + data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!, + pinned: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + cachedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}cached_at'])!, ); } @@ -1495,12 +1227,7 @@ class ApiCacheData extends DataClass implements Insertable { /// Timestamp for cache invalidation (optional future use) final DateTime cachedAt; - const ApiCacheData({ - required this.cacheKey, - required this.data, - required this.pinned, - required this.cachedAt, - }); + const ApiCacheData({required this.cacheKey, required this.data, required this.pinned, required this.cachedAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1520,10 +1247,7 @@ class ApiCacheData extends DataClass implements Insertable { ); } - factory ApiCacheData.fromJson( - Map json, { - ValueSerializer? serializer, - }) { + factory ApiCacheData.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return ApiCacheData( cacheKey: serializer.fromJson(json['cacheKey']), @@ -1543,12 +1267,7 @@ class ApiCacheData extends DataClass implements Insertable { }; } - ApiCacheData copyWith({ - String? cacheKey, - String? data, - bool? pinned, - DateTime? cachedAt, - }) => ApiCacheData( + ApiCacheData copyWith({String? cacheKey, String? data, bool? pinned, DateTime? cachedAt}) => ApiCacheData( cacheKey: cacheKey ?? this.cacheKey, data: data ?? this.data, pinned: pinned ?? this.pinned, @@ -1688,13 +1407,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'PRIMARY KEY AUTOINCREMENT', - ), - ); - static const VerificationMeta _serverIdMeta = const VerificationMeta( - 'serverId', + defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), ); + static const VerificationMeta _serverIdMeta = const VerificationMeta('serverId'); @override late final GeneratedColumn serverId = GeneratedColumn( 'server_id', @@ -1703,9 +1418,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _ratingKeyMeta = const VerificationMeta( - 'ratingKey', - ); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta('ratingKey'); @override late final GeneratedColumn ratingKey = GeneratedColumn( 'rating_key', @@ -1714,9 +1427,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _globalKeyMeta = const VerificationMeta( - 'globalKey', - ); + static const VerificationMeta _globalKeyMeta = const VerificationMeta('globalKey'); @override late final GeneratedColumn globalKey = GeneratedColumn( 'global_key', @@ -1725,9 +1436,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _actionTypeMeta = const VerificationMeta( - 'actionType', - ); + static const VerificationMeta _actionTypeMeta = const VerificationMeta('actionType'); @override late final GeneratedColumn actionType = GeneratedColumn( 'action_type', @@ -1736,9 +1445,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _viewOffsetMeta = const VerificationMeta( - 'viewOffset', - ); + static const VerificationMeta _viewOffsetMeta = const VerificationMeta('viewOffset'); @override late final GeneratedColumn viewOffset = GeneratedColumn( 'view_offset', @@ -1747,9 +1454,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _durationMeta = const VerificationMeta( - 'duration', - ); + static const VerificationMeta _durationMeta = const VerificationMeta('duration'); @override late final GeneratedColumn duration = GeneratedColumn( 'duration', @@ -1758,9 +1463,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _shouldMarkWatchedMeta = const VerificationMeta( - 'shouldMarkWatched', - ); + static const VerificationMeta _shouldMarkWatchedMeta = const VerificationMeta('shouldMarkWatched'); @override late final GeneratedColumn shouldMarkWatched = GeneratedColumn( 'should_mark_watched', @@ -1768,14 +1471,10 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("should_mark_watched" IN (0, 1))', - ), + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("should_mark_watched" IN (0, 1))'), defaultValue: const Constant(false), ); - static const VerificationMeta _createdAtMeta = const VerificationMeta( - 'createdAt', - ); + static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override late final GeneratedColumn createdAt = GeneratedColumn( 'created_at', @@ -1784,9 +1483,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _updatedAtMeta = const VerificationMeta( - 'updatedAt', - ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); @override late final GeneratedColumn updatedAt = GeneratedColumn( 'updated_at', @@ -1795,9 +1492,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _syncAttemptsMeta = const VerificationMeta( - 'syncAttempts', - ); + static const VerificationMeta _syncAttemptsMeta = const VerificationMeta('syncAttempts'); @override late final GeneratedColumn syncAttempts = GeneratedColumn( 'sync_attempts', @@ -1807,9 +1502,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _lastErrorMeta = const VerificationMeta( - 'lastError', - ); + static const VerificationMeta _lastErrorMeta = const VerificationMeta('lastError'); @override late final GeneratedColumn lastError = GeneratedColumn( 'last_error', @@ -1839,98 +1532,59 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress String get actualTableName => $name; static const String $name = 'offline_watch_progress'; @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } if (data.containsKey('server_id')) { - context.handle( - _serverIdMeta, - serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), - ); + context.handle(_serverIdMeta, serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta)); } else if (isInserting) { context.missing(_serverIdMeta); } if (data.containsKey('rating_key')) { - context.handle( - _ratingKeyMeta, - ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), - ); + context.handle(_ratingKeyMeta, ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta)); } else if (isInserting) { context.missing(_ratingKeyMeta); } if (data.containsKey('global_key')) { - context.handle( - _globalKeyMeta, - globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), - ); + context.handle(_globalKeyMeta, globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta)); } else if (isInserting) { context.missing(_globalKeyMeta); } if (data.containsKey('action_type')) { - context.handle( - _actionTypeMeta, - actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta), - ); + context.handle(_actionTypeMeta, actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta)); } else if (isInserting) { context.missing(_actionTypeMeta); } if (data.containsKey('view_offset')) { - context.handle( - _viewOffsetMeta, - viewOffset.isAcceptableOrUnknown(data['view_offset']!, _viewOffsetMeta), - ); + context.handle(_viewOffsetMeta, viewOffset.isAcceptableOrUnknown(data['view_offset']!, _viewOffsetMeta)); } if (data.containsKey('duration')) { - context.handle( - _durationMeta, - duration.isAcceptableOrUnknown(data['duration']!, _durationMeta), - ); + context.handle(_durationMeta, duration.isAcceptableOrUnknown(data['duration']!, _durationMeta)); } if (data.containsKey('should_mark_watched')) { context.handle( _shouldMarkWatchedMeta, - shouldMarkWatched.isAcceptableOrUnknown( - data['should_mark_watched']!, - _shouldMarkWatchedMeta, - ), + shouldMarkWatched.isAcceptableOrUnknown(data['should_mark_watched']!, _shouldMarkWatchedMeta), ); } if (data.containsKey('created_at')) { - context.handle( - _createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), - ); + context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } else if (isInserting) { context.missing(_createdAtMeta); } if (data.containsKey('updated_at')) { - context.handle( - _updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), - ); + context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } else if (isInserting) { context.missing(_updatedAtMeta); } if (data.containsKey('sync_attempts')) { - context.handle( - _syncAttemptsMeta, - syncAttempts.isAcceptableOrUnknown( - data['sync_attempts']!, - _syncAttemptsMeta, - ), - ); + context.handle(_syncAttemptsMeta, syncAttempts.isAcceptableOrUnknown(data['sync_attempts']!, _syncAttemptsMeta)); } if (data.containsKey('last_error')) { - context.handle( - _lastErrorMeta, - lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), - ); + context.handle(_lastErrorMeta, lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta)); } return context; } @@ -1938,60 +1592,24 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress @override Set get $primaryKey => {id}; @override - OfflineWatchProgressItem map( - Map data, { - String? tablePrefix, - }) { + OfflineWatchProgressItem map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return OfflineWatchProgressItem( - id: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}id'], - )!, - serverId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}server_id'], - )!, - ratingKey: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}rating_key'], - )!, - globalKey: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}global_key'], - )!, - actionType: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}action_type'], - )!, - viewOffset: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}view_offset'], - ), - duration: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}duration'], - ), + id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, + serverId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}server_id'])!, + ratingKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}rating_key'])!, + globalKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}global_key'])!, + actionType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}action_type'])!, + viewOffset: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}view_offset']), + duration: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration']), shouldMarkWatched: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}should_mark_watched'], )!, - createdAt: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}created_at'], - )!, - updatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}updated_at'], - )!, - syncAttempts: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}sync_attempts'], - )!, - lastError: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}last_error'], - ), + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}updated_at'])!, + syncAttempts: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}sync_attempts'])!, + lastError: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}last_error']), ); } @@ -2001,8 +1619,7 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress } } -class OfflineWatchProgressItem extends DataClass - implements Insertable { +class OfflineWatchProgressItem extends DataClass implements Insertable { /// Auto-incrementing primary key final int id; @@ -2084,26 +1701,17 @@ class OfflineWatchProgressItem extends DataClass ratingKey: Value(ratingKey), globalKey: Value(globalKey), actionType: Value(actionType), - viewOffset: viewOffset == null && nullToAbsent - ? const Value.absent() - : Value(viewOffset), - duration: duration == null && nullToAbsent - ? const Value.absent() - : Value(duration), + viewOffset: viewOffset == null && nullToAbsent ? const Value.absent() : Value(viewOffset), + duration: duration == null && nullToAbsent ? const Value.absent() : Value(duration), shouldMarkWatched: Value(shouldMarkWatched), createdAt: Value(createdAt), updatedAt: Value(updatedAt), syncAttempts: Value(syncAttempts), - lastError: lastError == null && nullToAbsent - ? const Value.absent() - : Value(lastError), + lastError: lastError == null && nullToAbsent ? const Value.absent() : Value(lastError), ); } - factory OfflineWatchProgressItem.fromJson( - Map json, { - ValueSerializer? serializer, - }) { + factory OfflineWatchProgressItem.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return OfflineWatchProgressItem( id: serializer.fromJson(json['id']), @@ -2166,29 +1774,19 @@ class OfflineWatchProgressItem extends DataClass syncAttempts: syncAttempts ?? this.syncAttempts, lastError: lastError.present ? lastError.value : this.lastError, ); - OfflineWatchProgressItem copyWithCompanion( - OfflineWatchProgressCompanion data, - ) { + OfflineWatchProgressItem copyWithCompanion(OfflineWatchProgressCompanion data) { return OfflineWatchProgressItem( id: data.id.present ? data.id.value : this.id, serverId: data.serverId.present ? data.serverId.value : this.serverId, ratingKey: data.ratingKey.present ? data.ratingKey.value : this.ratingKey, globalKey: data.globalKey.present ? data.globalKey.value : this.globalKey, - actionType: data.actionType.present - ? data.actionType.value - : this.actionType, - viewOffset: data.viewOffset.present - ? data.viewOffset.value - : this.viewOffset, + actionType: data.actionType.present ? data.actionType.value : this.actionType, + viewOffset: data.viewOffset.present ? data.viewOffset.value : this.viewOffset, duration: data.duration.present ? data.duration.value : this.duration, - shouldMarkWatched: data.shouldMarkWatched.present - ? data.shouldMarkWatched.value - : this.shouldMarkWatched, + shouldMarkWatched: data.shouldMarkWatched.present ? data.shouldMarkWatched.value : this.shouldMarkWatched, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, - syncAttempts: data.syncAttempts.present - ? data.syncAttempts.value - : this.syncAttempts, + syncAttempts: data.syncAttempts.present ? data.syncAttempts.value : this.syncAttempts, lastError: data.lastError.present ? data.lastError.value : this.lastError, ); } @@ -2245,8 +1843,7 @@ class OfflineWatchProgressItem extends DataClass other.lastError == this.lastError); } -class OfflineWatchProgressCompanion - extends UpdateCompanion { +class OfflineWatchProgressCompanion extends UpdateCompanion { final Value id; final Value serverId; final Value ratingKey; @@ -2417,23 +2014,14 @@ class OfflineWatchProgressCompanion abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); - late final $DownloadedMediaTable downloadedMedia = $DownloadedMediaTable( - this, - ); + late final $DownloadedMediaTable downloadedMedia = $DownloadedMediaTable(this); late final $DownloadQueueTable downloadQueue = $DownloadQueueTable(this); late final $ApiCacheTable apiCache = $ApiCacheTable(this); - late final $OfflineWatchProgressTable offlineWatchProgress = - $OfflineWatchProgressTable(this); + late final $OfflineWatchProgressTable offlineWatchProgress = $OfflineWatchProgressTable(this); @override - Iterable> get allTables => - allSchemaEntities.whereType>(); + Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [ - downloadedMedia, - downloadQueue, - apiCache, - offlineWatchProgress, - ]; + List get allSchemaEntities => [downloadedMedia, downloadQueue, apiCache, offlineWatchProgress]; } typedef $$DownloadedMediaTableCreateCompanionBuilder = @@ -2475,8 +2063,7 @@ typedef $$DownloadedMediaTableUpdateCompanionBuilder = Value retryCount, }); -class $$DownloadedMediaTableFilterComposer - extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableFilterComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableFilterComposer({ required super.$db, required super.$table, @@ -2484,89 +2071,54 @@ class $$DownloadedMediaTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get serverId => $composableBuilder( - column: $table.serverId, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => ColumnFilters(column)); - ColumnFilters get ratingKey => $composableBuilder( - column: $table.ratingKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get globalKey => $composableBuilder( - column: $table.globalKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get type => $composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column)); - ColumnFilters get parentRatingKey => $composableBuilder( - column: $table.parentRatingKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get parentRatingKey => + $composableBuilder(column: $table.parentRatingKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get grandparentRatingKey => $composableBuilder( - column: $table.grandparentRatingKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get grandparentRatingKey => + $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get status => $composableBuilder( - column: $table.status, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get status => + $composableBuilder(column: $table.status, builder: (column) => ColumnFilters(column)); - ColumnFilters get progress => $composableBuilder( - column: $table.progress, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get progress => + $composableBuilder(column: $table.progress, builder: (column) => ColumnFilters(column)); - ColumnFilters get totalBytes => $composableBuilder( - column: $table.totalBytes, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get totalBytes => + $composableBuilder(column: $table.totalBytes, builder: (column) => ColumnFilters(column)); - ColumnFilters get downloadedBytes => $composableBuilder( - column: $table.downloadedBytes, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get downloadedBytes => + $composableBuilder(column: $table.downloadedBytes, builder: (column) => ColumnFilters(column)); - ColumnFilters get videoFilePath => $composableBuilder( - column: $table.videoFilePath, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get videoFilePath => + $composableBuilder(column: $table.videoFilePath, builder: (column) => ColumnFilters(column)); - ColumnFilters get thumbPath => $composableBuilder( - column: $table.thumbPath, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get thumbPath => + $composableBuilder(column: $table.thumbPath, builder: (column) => ColumnFilters(column)); - ColumnFilters get downloadedAt => $composableBuilder( - column: $table.downloadedAt, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get downloadedAt => + $composableBuilder(column: $table.downloadedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get errorMessage => $composableBuilder( - column: $table.errorMessage, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get errorMessage => + $composableBuilder(column: $table.errorMessage, builder: (column) => ColumnFilters(column)); - ColumnFilters get retryCount => $composableBuilder( - column: $table.retryCount, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get retryCount => + $composableBuilder(column: $table.retryCount, builder: (column) => ColumnFilters(column)); } -class $$DownloadedMediaTableOrderingComposer - extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableOrderingComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableOrderingComposer({ required super.$db, required super.$table, @@ -2574,89 +2126,55 @@ class $$DownloadedMediaTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get serverId => $composableBuilder( - column: $table.serverId, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get ratingKey => $composableBuilder( - column: $table.ratingKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get globalKey => $composableBuilder( - column: $table.globalKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( - column: $table.type, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get type => + $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get parentRatingKey => $composableBuilder( - column: $table.parentRatingKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get parentRatingKey => + $composableBuilder(column: $table.parentRatingKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get grandparentRatingKey => $composableBuilder( - column: $table.grandparentRatingKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get grandparentRatingKey => + $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get status => $composableBuilder( - column: $table.status, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get status => + $composableBuilder(column: $table.status, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get progress => $composableBuilder( - column: $table.progress, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get progress => + $composableBuilder(column: $table.progress, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get totalBytes => $composableBuilder( - column: $table.totalBytes, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get totalBytes => + $composableBuilder(column: $table.totalBytes, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get downloadedBytes => $composableBuilder( - column: $table.downloadedBytes, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get downloadedBytes => + $composableBuilder(column: $table.downloadedBytes, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get videoFilePath => $composableBuilder( - column: $table.videoFilePath, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get videoFilePath => + $composableBuilder(column: $table.videoFilePath, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get thumbPath => $composableBuilder( - column: $table.thumbPath, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get thumbPath => + $composableBuilder(column: $table.thumbPath, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get downloadedAt => $composableBuilder( - column: $table.downloadedAt, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get downloadedAt => + $composableBuilder(column: $table.downloadedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get errorMessage => $composableBuilder( - column: $table.errorMessage, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get errorMessage => + $composableBuilder(column: $table.errorMessage, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get retryCount => $composableBuilder( - column: $table.retryCount, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get retryCount => + $composableBuilder(column: $table.retryCount, builder: (column) => ColumnOrderings(column)); } -class $$DownloadedMediaTableAnnotationComposer - extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableAnnotationComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableAnnotationComposer({ required super.$db, required super.$table, @@ -2664,69 +2182,42 @@ class $$DownloadedMediaTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => column); + GeneratedColumn get serverId => $composableBuilder(column: $table.serverId, builder: (column) => column); - GeneratedColumn get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => column); + GeneratedColumn get ratingKey => $composableBuilder(column: $table.ratingKey, builder: (column) => column); - GeneratedColumn get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => column); + GeneratedColumn get globalKey => $composableBuilder(column: $table.globalKey, builder: (column) => column); - GeneratedColumn get type => - $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); - GeneratedColumn get parentRatingKey => $composableBuilder( - column: $table.parentRatingKey, - builder: (column) => column, - ); + GeneratedColumn get parentRatingKey => + $composableBuilder(column: $table.parentRatingKey, builder: (column) => column); - GeneratedColumn get grandparentRatingKey => $composableBuilder( - column: $table.grandparentRatingKey, - builder: (column) => column, - ); + GeneratedColumn get grandparentRatingKey => + $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => column); - GeneratedColumn get status => - $composableBuilder(column: $table.status, builder: (column) => column); + GeneratedColumn get status => $composableBuilder(column: $table.status, builder: (column) => column); - GeneratedColumn get progress => - $composableBuilder(column: $table.progress, builder: (column) => column); + GeneratedColumn get progress => $composableBuilder(column: $table.progress, builder: (column) => column); - GeneratedColumn get totalBytes => $composableBuilder( - column: $table.totalBytes, - builder: (column) => column, - ); + GeneratedColumn get totalBytes => $composableBuilder(column: $table.totalBytes, builder: (column) => column); - GeneratedColumn get downloadedBytes => $composableBuilder( - column: $table.downloadedBytes, - builder: (column) => column, - ); + GeneratedColumn get downloadedBytes => + $composableBuilder(column: $table.downloadedBytes, builder: (column) => column); - GeneratedColumn get videoFilePath => $composableBuilder( - column: $table.videoFilePath, - builder: (column) => column, - ); + GeneratedColumn get videoFilePath => + $composableBuilder(column: $table.videoFilePath, builder: (column) => column); - GeneratedColumn get thumbPath => - $composableBuilder(column: $table.thumbPath, builder: (column) => column); + GeneratedColumn get thumbPath => $composableBuilder(column: $table.thumbPath, builder: (column) => column); - GeneratedColumn get downloadedAt => $composableBuilder( - column: $table.downloadedAt, - builder: (column) => column, - ); + GeneratedColumn get downloadedAt => $composableBuilder(column: $table.downloadedAt, builder: (column) => column); - GeneratedColumn get errorMessage => $composableBuilder( - column: $table.errorMessage, - builder: (column) => column, - ); + GeneratedColumn get errorMessage => + $composableBuilder(column: $table.errorMessage, builder: (column) => column); - GeneratedColumn get retryCount => $composableBuilder( - column: $table.retryCount, - builder: (column) => column, - ); + GeneratedColumn get retryCount => $composableBuilder(column: $table.retryCount, builder: (column) => column); } class $$DownloadedMediaTableTableManager @@ -2740,30 +2231,18 @@ class $$DownloadedMediaTableTableManager $$DownloadedMediaTableAnnotationComposer, $$DownloadedMediaTableCreateCompanionBuilder, $$DownloadedMediaTableUpdateCompanionBuilder, - ( - DownloadedMediaItem, - BaseReferences< - _$AppDatabase, - $DownloadedMediaTable, - DownloadedMediaItem - >, - ), + (DownloadedMediaItem, BaseReferences<_$AppDatabase, $DownloadedMediaTable, DownloadedMediaItem>), DownloadedMediaItem, PrefetchHooks Function() > { - $$DownloadedMediaTableTableManager( - _$AppDatabase db, - $DownloadedMediaTable table, - ) : super( + $$DownloadedMediaTableTableManager(_$AppDatabase db, $DownloadedMediaTable table) + : super( TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$DownloadedMediaTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$DownloadedMediaTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$DownloadedMediaTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => $$DownloadedMediaTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$DownloadedMediaTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$DownloadedMediaTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -2836,9 +2315,7 @@ class $$DownloadedMediaTableTableManager errorMessage: errorMessage, retryCount: retryCount, ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), prefetchHooksCallback: null, ), ); @@ -2854,14 +2331,7 @@ typedef $$DownloadedMediaTableProcessedTableManager = $$DownloadedMediaTableAnnotationComposer, $$DownloadedMediaTableCreateCompanionBuilder, $$DownloadedMediaTableUpdateCompanionBuilder, - ( - DownloadedMediaItem, - BaseReferences< - _$AppDatabase, - $DownloadedMediaTable, - DownloadedMediaItem - >, - ), + (DownloadedMediaItem, BaseReferences<_$AppDatabase, $DownloadedMediaTable, DownloadedMediaItem>), DownloadedMediaItem, PrefetchHooks Function() >; @@ -2884,8 +2354,7 @@ typedef $$DownloadQueueTableUpdateCompanionBuilder = Value downloadArtwork, }); -class $$DownloadQueueTableFilterComposer - extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableFilterComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableFilterComposer({ required super.$db, required super.$table, @@ -2893,39 +2362,25 @@ class $$DownloadQueueTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get mediaGlobalKey => $composableBuilder( - column: $table.mediaGlobalKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get mediaGlobalKey => + $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get priority => $composableBuilder( - column: $table.priority, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get priority => + $composableBuilder(column: $table.priority, builder: (column) => ColumnFilters(column)); - ColumnFilters get addedAt => $composableBuilder( - column: $table.addedAt, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get addedAt => + $composableBuilder(column: $table.addedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get downloadSubtitles => $composableBuilder( - column: $table.downloadSubtitles, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get downloadSubtitles => + $composableBuilder(column: $table.downloadSubtitles, builder: (column) => ColumnFilters(column)); - ColumnFilters get downloadArtwork => $composableBuilder( - column: $table.downloadArtwork, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get downloadArtwork => + $composableBuilder(column: $table.downloadArtwork, builder: (column) => ColumnFilters(column)); } -class $$DownloadQueueTableOrderingComposer - extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableOrderingComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableOrderingComposer({ required super.$db, required super.$table, @@ -2933,39 +2388,25 @@ class $$DownloadQueueTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get mediaGlobalKey => $composableBuilder( - column: $table.mediaGlobalKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get mediaGlobalKey => + $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get priority => $composableBuilder( - column: $table.priority, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get priority => + $composableBuilder(column: $table.priority, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get addedAt => $composableBuilder( - column: $table.addedAt, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get addedAt => + $composableBuilder(column: $table.addedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get downloadSubtitles => $composableBuilder( - column: $table.downloadSubtitles, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get downloadSubtitles => + $composableBuilder(column: $table.downloadSubtitles, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get downloadArtwork => $composableBuilder( - column: $table.downloadArtwork, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get downloadArtwork => + $composableBuilder(column: $table.downloadArtwork, builder: (column) => ColumnOrderings(column)); } -class $$DownloadQueueTableAnnotationComposer - extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableAnnotationComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableAnnotationComposer({ required super.$db, required super.$table, @@ -2973,29 +2414,20 @@ class $$DownloadQueueTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get mediaGlobalKey => $composableBuilder( - column: $table.mediaGlobalKey, - builder: (column) => column, - ); + GeneratedColumn get mediaGlobalKey => + $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => column); - GeneratedColumn get priority => - $composableBuilder(column: $table.priority, builder: (column) => column); + GeneratedColumn get priority => $composableBuilder(column: $table.priority, builder: (column) => column); - GeneratedColumn get addedAt => - $composableBuilder(column: $table.addedAt, builder: (column) => column); + GeneratedColumn get addedAt => $composableBuilder(column: $table.addedAt, builder: (column) => column); - GeneratedColumn get downloadSubtitles => $composableBuilder( - column: $table.downloadSubtitles, - builder: (column) => column, - ); + GeneratedColumn get downloadSubtitles => + $composableBuilder(column: $table.downloadSubtitles, builder: (column) => column); - GeneratedColumn get downloadArtwork => $composableBuilder( - column: $table.downloadArtwork, - builder: (column) => column, - ); + GeneratedColumn get downloadArtwork => + $composableBuilder(column: $table.downloadArtwork, builder: (column) => column); } class $$DownloadQueueTableTableManager @@ -3009,14 +2441,7 @@ class $$DownloadQueueTableTableManager $$DownloadQueueTableAnnotationComposer, $$DownloadQueueTableCreateCompanionBuilder, $$DownloadQueueTableUpdateCompanionBuilder, - ( - DownloadQueueItem, - BaseReferences< - _$AppDatabase, - $DownloadQueueTable, - DownloadQueueItem - >, - ), + (DownloadQueueItem, BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>), DownloadQueueItem, PrefetchHooks Function() > { @@ -3025,12 +2450,9 @@ class $$DownloadQueueTableTableManager TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$DownloadQueueTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$DownloadQueueTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$DownloadQueueTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => $$DownloadQueueTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$DownloadQueueTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$DownloadQueueTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -3063,9 +2485,7 @@ class $$DownloadQueueTableTableManager downloadSubtitles: downloadSubtitles, downloadArtwork: downloadArtwork, ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), prefetchHooksCallback: null, ), ); @@ -3081,10 +2501,7 @@ typedef $$DownloadQueueTableProcessedTableManager = $$DownloadQueueTableAnnotationComposer, $$DownloadQueueTableCreateCompanionBuilder, $$DownloadQueueTableUpdateCompanionBuilder, - ( - DownloadQueueItem, - BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>, - ), + (DownloadQueueItem, BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>), DownloadQueueItem, PrefetchHooks Function() >; @@ -3105,8 +2522,7 @@ typedef $$ApiCacheTableUpdateCompanionBuilder = Value rowid, }); -class $$ApiCacheTableFilterComposer - extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableFilterComposer extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableFilterComposer({ required super.$db, required super.$table, @@ -3114,29 +2530,19 @@ class $$ApiCacheTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get cacheKey => $composableBuilder( - column: $table.cacheKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get cacheKey => + $composableBuilder(column: $table.cacheKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get data => $composableBuilder( - column: $table.data, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get data => $composableBuilder(column: $table.data, builder: (column) => ColumnFilters(column)); - ColumnFilters get pinned => $composableBuilder( - column: $table.pinned, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => ColumnFilters(column)); - ColumnFilters get cachedAt => $composableBuilder( - column: $table.cachedAt, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get cachedAt => + $composableBuilder(column: $table.cachedAt, builder: (column) => ColumnFilters(column)); } -class $$ApiCacheTableOrderingComposer - extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableOrderingComposer extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableOrderingComposer({ required super.$db, required super.$table, @@ -3144,29 +2550,20 @@ class $$ApiCacheTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get cacheKey => $composableBuilder( - column: $table.cacheKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get cacheKey => + $composableBuilder(column: $table.cacheKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get data => $composableBuilder( - column: $table.data, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get data => + $composableBuilder(column: $table.data, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinned => $composableBuilder( - column: $table.pinned, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get cachedAt => $composableBuilder( - column: $table.cachedAt, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get cachedAt => + $composableBuilder(column: $table.cachedAt, builder: (column) => ColumnOrderings(column)); } -class $$ApiCacheTableAnnotationComposer - extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableAnnotationComposer extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableAnnotationComposer({ required super.$db, required super.$table, @@ -3174,17 +2571,13 @@ class $$ApiCacheTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get cacheKey => - $composableBuilder(column: $table.cacheKey, builder: (column) => column); + GeneratedColumn get cacheKey => $composableBuilder(column: $table.cacheKey, builder: (column) => column); - GeneratedColumn get data => - $composableBuilder(column: $table.data, builder: (column) => column); + GeneratedColumn get data => $composableBuilder(column: $table.data, builder: (column) => column); - GeneratedColumn get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => column); + GeneratedColumn get pinned => $composableBuilder(column: $table.pinned, builder: (column) => column); - GeneratedColumn get cachedAt => - $composableBuilder(column: $table.cachedAt, builder: (column) => column); + GeneratedColumn get cachedAt => $composableBuilder(column: $table.cachedAt, builder: (column) => column); } class $$ApiCacheTableTableManager @@ -3198,10 +2591,7 @@ class $$ApiCacheTableTableManager $$ApiCacheTableAnnotationComposer, $$ApiCacheTableCreateCompanionBuilder, $$ApiCacheTableUpdateCompanionBuilder, - ( - ApiCacheData, - BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, - ), + (ApiCacheData, BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>), ApiCacheData, PrefetchHooks Function() > { @@ -3210,12 +2600,9 @@ class $$ApiCacheTableTableManager TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$ApiCacheTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ApiCacheTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ApiCacheTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => $$ApiCacheTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$ApiCacheTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$ApiCacheTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value cacheKey = const Value.absent(), @@ -3223,13 +2610,7 @@ class $$ApiCacheTableTableManager Value pinned = const Value.absent(), Value cachedAt = const Value.absent(), Value rowid = const Value.absent(), - }) => ApiCacheCompanion( - cacheKey: cacheKey, - data: data, - pinned: pinned, - cachedAt: cachedAt, - rowid: rowid, - ), + }) => ApiCacheCompanion(cacheKey: cacheKey, data: data, pinned: pinned, cachedAt: cachedAt, rowid: rowid), createCompanionCallback: ({ required String cacheKey, @@ -3244,9 +2625,7 @@ class $$ApiCacheTableTableManager cachedAt: cachedAt, rowid: rowid, ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), prefetchHooksCallback: null, ), ); @@ -3262,10 +2641,7 @@ typedef $$ApiCacheTableProcessedTableManager = $$ApiCacheTableAnnotationComposer, $$ApiCacheTableCreateCompanionBuilder, $$ApiCacheTableUpdateCompanionBuilder, - ( - ApiCacheData, - BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, - ), + (ApiCacheData, BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>), ApiCacheData, PrefetchHooks Function() >; @@ -3300,8 +2676,7 @@ typedef $$OfflineWatchProgressTableUpdateCompanionBuilder = Value lastError, }); -class $$OfflineWatchProgressTableFilterComposer - extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableFilterComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableFilterComposer({ required super.$db, required super.$table, @@ -3309,69 +2684,43 @@ class $$OfflineWatchProgressTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get serverId => $composableBuilder( - column: $table.serverId, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => ColumnFilters(column)); - ColumnFilters get ratingKey => $composableBuilder( - column: $table.ratingKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get globalKey => $composableBuilder( - column: $table.globalKey, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => ColumnFilters(column)); - ColumnFilters get actionType => $composableBuilder( - column: $table.actionType, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get actionType => + $composableBuilder(column: $table.actionType, builder: (column) => ColumnFilters(column)); - ColumnFilters get viewOffset => $composableBuilder( - column: $table.viewOffset, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get viewOffset => + $composableBuilder(column: $table.viewOffset, builder: (column) => ColumnFilters(column)); - ColumnFilters get duration => $composableBuilder( - column: $table.duration, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get duration => + $composableBuilder(column: $table.duration, builder: (column) => ColumnFilters(column)); - ColumnFilters get shouldMarkWatched => $composableBuilder( - column: $table.shouldMarkWatched, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get shouldMarkWatched => + $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get syncAttempts => $composableBuilder( - column: $table.syncAttempts, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get syncAttempts => + $composableBuilder(column: $table.syncAttempts, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastError => $composableBuilder( - column: $table.lastError, - builder: (column) => ColumnFilters(column), - ); + ColumnFilters get lastError => + $composableBuilder(column: $table.lastError, builder: (column) => ColumnFilters(column)); } -class $$OfflineWatchProgressTableOrderingComposer - extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableOrderingComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableOrderingComposer({ required super.$db, required super.$table, @@ -3379,69 +2728,43 @@ class $$OfflineWatchProgressTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get serverId => $composableBuilder( - column: $table.serverId, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get ratingKey => $composableBuilder( - column: $table.ratingKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get globalKey => $composableBuilder( - column: $table.globalKey, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get actionType => $composableBuilder( - column: $table.actionType, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get actionType => + $composableBuilder(column: $table.actionType, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get viewOffset => $composableBuilder( - column: $table.viewOffset, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get viewOffset => + $composableBuilder(column: $table.viewOffset, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get duration => $composableBuilder( - column: $table.duration, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get duration => + $composableBuilder(column: $table.duration, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get shouldMarkWatched => $composableBuilder( - column: $table.shouldMarkWatched, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get shouldMarkWatched => + $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get syncAttempts => $composableBuilder( - column: $table.syncAttempts, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get syncAttempts => + $composableBuilder(column: $table.syncAttempts, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastError => $composableBuilder( - column: $table.lastError, - builder: (column) => ColumnOrderings(column), - ); + ColumnOrderings get lastError => + $composableBuilder(column: $table.lastError, builder: (column) => ColumnOrderings(column)); } -class $$OfflineWatchProgressTableAnnotationComposer - extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableAnnotationComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableAnnotationComposer({ required super.$db, required super.$table, @@ -3449,49 +2772,30 @@ class $$OfflineWatchProgressTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => column); + GeneratedColumn get serverId => $composableBuilder(column: $table.serverId, builder: (column) => column); - GeneratedColumn get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => column); + GeneratedColumn get ratingKey => $composableBuilder(column: $table.ratingKey, builder: (column) => column); - GeneratedColumn get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => column); + GeneratedColumn get globalKey => $composableBuilder(column: $table.globalKey, builder: (column) => column); - GeneratedColumn get actionType => $composableBuilder( - column: $table.actionType, - builder: (column) => column, - ); + GeneratedColumn get actionType => $composableBuilder(column: $table.actionType, builder: (column) => column); - GeneratedColumn get viewOffset => $composableBuilder( - column: $table.viewOffset, - builder: (column) => column, - ); + GeneratedColumn get viewOffset => $composableBuilder(column: $table.viewOffset, builder: (column) => column); - GeneratedColumn get duration => - $composableBuilder(column: $table.duration, builder: (column) => column); + GeneratedColumn get duration => $composableBuilder(column: $table.duration, builder: (column) => column); - GeneratedColumn get shouldMarkWatched => $composableBuilder( - column: $table.shouldMarkWatched, - builder: (column) => column, - ); + GeneratedColumn get shouldMarkWatched => + $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumn get syncAttempts => $composableBuilder( - column: $table.syncAttempts, - builder: (column) => column, - ); + GeneratedColumn get syncAttempts => $composableBuilder(column: $table.syncAttempts, builder: (column) => column); - GeneratedColumn get lastError => - $composableBuilder(column: $table.lastError, builder: (column) => column); + GeneratedColumn get lastError => $composableBuilder(column: $table.lastError, builder: (column) => column); } class $$OfflineWatchProgressTableTableManager @@ -3507,34 +2811,19 @@ class $$OfflineWatchProgressTableTableManager $$OfflineWatchProgressTableUpdateCompanionBuilder, ( OfflineWatchProgressItem, - BaseReferences< - _$AppDatabase, - $OfflineWatchProgressTable, - OfflineWatchProgressItem - >, + BaseReferences<_$AppDatabase, $OfflineWatchProgressTable, OfflineWatchProgressItem>, ), OfflineWatchProgressItem, PrefetchHooks Function() > { - $$OfflineWatchProgressTableTableManager( - _$AppDatabase db, - $OfflineWatchProgressTable table, - ) : super( + $$OfflineWatchProgressTableTableManager(_$AppDatabase db, $OfflineWatchProgressTable table) + : super( TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$OfflineWatchProgressTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$OfflineWatchProgressTableOrderingComposer( - $db: db, - $table: table, - ), - createComputedFieldComposer: () => - $$OfflineWatchProgressTableAnnotationComposer( - $db: db, - $table: table, - ), + createFilteringComposer: () => $$OfflineWatchProgressTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$OfflineWatchProgressTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$OfflineWatchProgressTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -3591,9 +2880,7 @@ class $$OfflineWatchProgressTableTableManager syncAttempts: syncAttempts, lastError: lastError, ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), prefetchHooksCallback: null, ), ); @@ -3609,14 +2896,7 @@ typedef $$OfflineWatchProgressTableProcessedTableManager = $$OfflineWatchProgressTableAnnotationComposer, $$OfflineWatchProgressTableCreateCompanionBuilder, $$OfflineWatchProgressTableUpdateCompanionBuilder, - ( - OfflineWatchProgressItem, - BaseReferences< - _$AppDatabase, - $OfflineWatchProgressTable, - OfflineWatchProgressItem - >, - ), + (OfflineWatchProgressItem, BaseReferences<_$AppDatabase, $OfflineWatchProgressTable, OfflineWatchProgressItem>), OfflineWatchProgressItem, PrefetchHooks Function() >; @@ -3626,10 +2906,8 @@ class $AppDatabaseManager { $AppDatabaseManager(this._db); $$DownloadedMediaTableTableManager get downloadedMedia => $$DownloadedMediaTableTableManager(_db, _db.downloadedMedia); - $$DownloadQueueTableTableManager get downloadQueue => - $$DownloadQueueTableTableManager(_db, _db.downloadQueue); - $$ApiCacheTableTableManager get apiCache => - $$ApiCacheTableTableManager(_db, _db.apiCache); + $$DownloadQueueTableTableManager get downloadQueue => $$DownloadQueueTableTableManager(_db, _db.downloadQueue); + $$ApiCacheTableTableManager get apiCache => $$ApiCacheTableTableManager(_db, _db.apiCache); $$OfflineWatchProgressTableTableManager get offlineWatchProgress => $$OfflineWatchProgressTableTableManager(_db, _db.offlineWatchProgress); } diff --git a/lib/models/companion_remote/recent_remote_session.dart b/lib/models/companion_remote/recent_remote_session.dart index b49f7213..18a72877 100644 --- a/lib/models/companion_remote/recent_remote_session.dart +++ b/lib/models/companion_remote/recent_remote_session.dart @@ -10,6 +10,7 @@ class RecentRemoteSession { final String deviceName; final String platform; final DateTime lastConnected; + final String? hostAddress; // Format: "ip:port" RecentRemoteSession({ required this.sessionId, @@ -17,25 +18,32 @@ class RecentRemoteSession { required this.deviceName, required this.platform, required this.lastConnected, + this.hostAddress, }); factory RecentRemoteSession.fromJson(Map json) => _$RecentRemoteSessionFromJson(json); Map 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) { - final parts = qrData.split(':'); - if (parts.length < 2) { - throw FormatException('Invalid QR code format'); + final parts = qrData.split('|'); + if (parts.length < 4) { + 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( - sessionId: parts[0], - pin: parts[1], - deviceName: parts.length > 2 ? parts[2] : 'Unknown Device', - platform: parts.length > 3 ? parts[3] : 'unknown', + sessionId: sessionId, + pin: pin, + deviceName: 'Unknown Device', + platform: 'unknown', lastConnected: DateTime.now(), + hostAddress: '$ip:$port', ); } diff --git a/lib/models/companion_remote/recent_remote_session.g.dart b/lib/models/companion_remote/recent_remote_session.g.dart index 6f5f0685..4a97dfe5 100644 --- a/lib/models/companion_remote/recent_remote_session.g.dart +++ b/lib/models/companion_remote/recent_remote_session.g.dart @@ -12,6 +12,7 @@ RecentRemoteSession _$RecentRemoteSessionFromJson(Map json) => deviceName: json['deviceName'] as String, platform: json['platform'] as String, lastConnected: DateTime.parse(json['lastConnected'] as String), + hostAddress: json['hostAddress'] as String?, ); Map _$RecentRemoteSessionToJson(RecentRemoteSession instance) => { @@ -20,4 +21,5 @@ Map _$RecentRemoteSessionToJson(RecentRemoteSession instance) = 'deviceName': instance.deviceName, 'platform': instance.platform, 'lastConnected': instance.lastConnected.toIso8601String(), + 'hostAddress': instance.hostAddress, }; diff --git a/lib/models/companion_remote/remote_session.dart b/lib/models/companion_remote/remote_session.dart index a984ac7d..e7b98c4b 100644 --- a/lib/models/companion_remote/remote_session.dart +++ b/lib/models/companion_remote/remote_session.dart @@ -90,17 +90,19 @@ class RemoteSession { RemoteSessionRole? role, RemoteSessionStatus? status, RemoteDevice? connectedDevice, + bool clearConnectedDevice = false, DateTime? createdAt, String? errorMessage, + bool clearErrorMessage = false, }) { return RemoteSession( sessionId: sessionId ?? this.sessionId, pin: pin ?? this.pin, role: role ?? this.role, status: status ?? this.status, - connectedDevice: connectedDevice ?? this.connectedDevice, + connectedDevice: clearConnectedDevice ? null : (connectedDevice ?? this.connectedDevice), createdAt: createdAt ?? this.createdAt, - errorMessage: errorMessage ?? this.errorMessage, + errorMessage: clearErrorMessage ? null : (errorMessage ?? this.errorMessage), ); } } diff --git a/lib/models/play_queue_response.g.dart b/lib/models/play_queue_response.g.dart index 8f8e3aa9..7a197697 100644 --- a/lib/models/play_queue_response.g.dart +++ b/lib/models/play_queue_response.g.dart @@ -6,23 +6,15 @@ part of 'play_queue_response.dart'; // JsonSerializableGenerator // ************************************************************************** -PlayQueueResponse _$PlayQueueResponseFromJson(Map json) => - PlayQueueResponse( - playQueueID: (json['playQueueID'] as num).toInt(), - playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?) - ?.toInt(), - playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?) - ?.toInt(), - playQueueSelectedMetadataItemID: - json['playQueueSelectedMetadataItemID'] as String?, - playQueueShuffled: const BoolOrIntConverter().fromJson( - json['playQueueShuffled'] as Object, - ), - 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?) - ?.map((e) => PlexMetadata.fromJson(e as Map)) - .toList(), - ); +PlayQueueResponse _$PlayQueueResponseFromJson(Map json) => PlayQueueResponse( + playQueueID: (json['playQueueID'] as num).toInt(), + playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?)?.toInt(), + playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?)?.toInt(), + playQueueSelectedMetadataItemID: json['playQueueSelectedMetadataItemID'] as String?, + playQueueShuffled: const BoolOrIntConverter().fromJson(json['playQueueShuffled'] as Object), + 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?)?.map((e) => PlexMetadata.fromJson(e as Map)).toList(), +); diff --git a/lib/models/plex_library.g.dart b/lib/models/plex_library.g.dart index 78565719..cbca367d 100644 --- a/lib/models/plex_library.g.dart +++ b/lib/models/plex_library.g.dart @@ -19,16 +19,15 @@ PlexLibrary _$PlexLibraryFromJson(Map json) => PlexLibrary( hidden: (json['hidden'] as num?)?.toInt(), ); -Map _$PlexLibraryToJson(PlexLibrary instance) => - { - 'key': instance.key, - 'title': instance.title, - 'type': instance.type, - 'agent': instance.agent, - 'scanner': instance.scanner, - 'language': instance.language, - 'uuid': instance.uuid, - 'updatedAt': instance.updatedAt, - 'createdAt': instance.createdAt, - 'hidden': instance.hidden, - }; +Map _$PlexLibraryToJson(PlexLibrary instance) => { + 'key': instance.key, + 'title': instance.title, + 'type': instance.type, + 'agent': instance.agent, + 'scanner': instance.scanner, + 'language': instance.language, + 'uuid': instance.uuid, + 'updatedAt': instance.updatedAt, + 'createdAt': instance.createdAt, + 'hidden': instance.hidden, +}; diff --git a/lib/models/plex_metadata.g.dart b/lib/models/plex_metadata.g.dart index adf96e91..d67a5fe4 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -41,9 +41,7 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( leafCount: (json['leafCount'] as num?)?.toInt(), viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(), childCount: (json['childCount'] as num?)?.toInt(), - role: (json['Role'] as List?) - ?.map((e) => PlexRole.fromJson(e as Map)) - .toList(), + role: (json['Role'] as List?)?.map((e) => PlexRole.fromJson(e as Map)).toList(), audioLanguage: json['audioLanguage'] as String?, subtitleLanguage: json['subtitleLanguage'] as String?, playlistItemID: (json['playlistItemID'] as num?)?.toInt(), @@ -54,49 +52,48 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( clearLogo: json['clearLogo'] as String?, ); -Map _$PlexMetadataToJson(PlexMetadata instance) => - { - 'ratingKey': instance.ratingKey, - 'key': instance.key, - 'guid': instance.guid, - 'studio': instance.studio, - 'type': instance.type, - 'title': instance.title, - 'titleSort': instance.titleSort, - 'contentRating': instance.contentRating, - 'summary': instance.summary, - 'rating': instance.rating, - 'audienceRating': instance.audienceRating, - 'year': instance.year, - 'originallyAvailableAt': instance.originallyAvailableAt, - 'thumb': instance.thumb, - 'art': instance.art, - 'duration': instance.duration, - 'addedAt': instance.addedAt, - 'updatedAt': instance.updatedAt, - 'lastViewedAt': instance.lastViewedAt, - 'grandparentTitle': instance.grandparentTitle, - 'grandparentThumb': instance.grandparentThumb, - 'grandparentArt': instance.grandparentArt, - 'grandparentRatingKey': instance.grandparentRatingKey, - 'parentTitle': instance.parentTitle, - 'parentThumb': instance.parentThumb, - 'parentRatingKey': instance.parentRatingKey, - 'parentIndex': instance.parentIndex, - 'index': instance.index, - 'grandparentTheme': instance.grandparentTheme, - 'viewOffset': instance.viewOffset, - 'viewCount': instance.viewCount, - 'leafCount': instance.leafCount, - 'viewedLeafCount': instance.viewedLeafCount, - 'childCount': instance.childCount, - 'Role': instance.role, - 'audioLanguage': instance.audioLanguage, - 'subtitleLanguage': instance.subtitleLanguage, - 'playlistItemID': instance.playlistItemID, - 'playQueueItemID': instance.playQueueItemID, - 'librarySectionID': instance.librarySectionID, - 'ratingImage': instance.ratingImage, - 'audienceRatingImage': instance.audienceRatingImage, - 'clearLogo': instance.clearLogo, - }; +Map _$PlexMetadataToJson(PlexMetadata instance) => { + 'ratingKey': instance.ratingKey, + 'key': instance.key, + 'guid': instance.guid, + 'studio': instance.studio, + 'type': instance.type, + 'title': instance.title, + 'titleSort': instance.titleSort, + 'contentRating': instance.contentRating, + 'summary': instance.summary, + 'rating': instance.rating, + 'audienceRating': instance.audienceRating, + 'year': instance.year, + 'originallyAvailableAt': instance.originallyAvailableAt, + 'thumb': instance.thumb, + 'art': instance.art, + 'duration': instance.duration, + 'addedAt': instance.addedAt, + 'updatedAt': instance.updatedAt, + 'lastViewedAt': instance.lastViewedAt, + 'grandparentTitle': instance.grandparentTitle, + 'grandparentThumb': instance.grandparentThumb, + 'grandparentArt': instance.grandparentArt, + 'grandparentRatingKey': instance.grandparentRatingKey, + 'parentTitle': instance.parentTitle, + 'parentThumb': instance.parentThumb, + 'parentRatingKey': instance.parentRatingKey, + 'parentIndex': instance.parentIndex, + 'index': instance.index, + 'grandparentTheme': instance.grandparentTheme, + 'viewOffset': instance.viewOffset, + 'viewCount': instance.viewCount, + 'leafCount': instance.leafCount, + 'viewedLeafCount': instance.viewedLeafCount, + 'childCount': instance.childCount, + 'Role': instance.role, + 'audioLanguage': instance.audioLanguage, + 'subtitleLanguage': instance.subtitleLanguage, + 'playlistItemID': instance.playlistItemID, + 'playQueueItemID': instance.playQueueItemID, + 'librarySectionID': instance.librarySectionID, + 'ratingImage': instance.ratingImage, + 'audienceRatingImage': instance.audienceRatingImage, + 'clearLogo': instance.clearLogo, +}; diff --git a/lib/models/plex_playlist.g.dart b/lib/models/plex_playlist.g.dart index 96647865..a21f8f84 100644 --- a/lib/models/plex_playlist.g.dart +++ b/lib/models/plex_playlist.g.dart @@ -26,23 +26,22 @@ PlexPlaylist _$PlexPlaylistFromJson(Map json) => PlexPlaylist( thumb: json['thumb'] as String?, ); -Map _$PlexPlaylistToJson(PlexPlaylist instance) => - { - 'ratingKey': instance.ratingKey, - 'key': instance.key, - 'type': instance.type, - 'title': instance.title, - 'summary': instance.summary, - 'smart': instance.smart, - 'playlistType': instance.playlistType, - 'duration': instance.duration, - 'leafCount': instance.leafCount, - 'composite': instance.composite, - 'addedAt': instance.addedAt, - 'updatedAt': instance.updatedAt, - 'lastViewedAt': instance.lastViewedAt, - 'viewCount': instance.viewCount, - 'content': instance.content, - 'guid': instance.guid, - 'thumb': instance.thumb, - }; +Map _$PlexPlaylistToJson(PlexPlaylist instance) => { + 'ratingKey': instance.ratingKey, + 'key': instance.key, + 'type': instance.type, + 'title': instance.title, + 'summary': instance.summary, + 'smart': instance.smart, + 'playlistType': instance.playlistType, + 'duration': instance.duration, + 'leafCount': instance.leafCount, + 'composite': instance.composite, + 'addedAt': instance.addedAt, + 'updatedAt': instance.updatedAt, + 'lastViewedAt': instance.lastViewedAt, + 'viewCount': instance.viewCount, + 'content': instance.content, + 'guid': instance.guid, + 'thumb': instance.thumb, +}; diff --git a/lib/providers/companion_remote_provider.dart b/lib/providers/companion_remote_provider.dart index f44935f3..01416abc 100644 --- a/lib/providers/companion_remote_provider.dart +++ b/lib/providers/companion_remote_provider.dart @@ -36,6 +36,7 @@ class CompanionRemoteProvider with ChangeNotifier { bool _intentionalDisconnect = false; String? _lastSessionId; String? _lastPin; + String? _lastHostAddress; int get reconnectAttempts => _reconnectAttempts; @@ -130,7 +131,10 @@ class CompanionRemoteProvider with ChangeNotifier { _deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) { appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)'); if (_intentionalDisconnect) { - _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, connectedDevice: null); + _session = _session?.copyWith( + status: RemoteSessionStatus.disconnected, + clearConnectedDevice: true, + ); notifyListeners(); } else { _session = _session?.copyWith(status: RemoteSessionStatus.reconnecting); @@ -182,7 +186,7 @@ class CompanionRemoteProvider with ChangeNotifier { _statusSubscription = null; } - Future<({String sessionId, String pin})> createSession() async { + Future<({String sessionId, String pin, String address})> createSession() async { await leaveSession(); appLogger.d('CompanionRemote: Creating session as host'); @@ -201,7 +205,9 @@ class CompanionRemoteProvider with ChangeNotifier { ); 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; } catch (e) { @@ -218,13 +224,14 @@ class CompanionRemoteProvider with ChangeNotifier { } } - Future joinSession(String sessionId, String pin) async { + Future joinSession(String sessionId, String pin, String hostAddress) async { await leaveSession(); _lastSessionId = sessionId; _lastPin = pin; + _lastHostAddress = hostAddress; - appLogger.d('CompanionRemote: Joining session - ID: $sessionId'); + appLogger.d('CompanionRemote: Joining session - ID: $sessionId, Host: $hostAddress'); _peerService = CompanionRemotePeerService(); _setupPeerServiceListeners(); @@ -238,7 +245,7 @@ class CompanionRemoteProvider with ChangeNotifier { notifyListeners(); try { - await _peerService!.joinSession(sessionId, pin, _deviceName, _platform); + await _peerService!.joinSession(sessionId, pin, _deviceName, _platform, hostAddress); _session = _session?.copyWith(status: RemoteSessionStatus.connected); notifyListeners(); @@ -289,7 +296,7 @@ class CompanionRemoteProvider with ChangeNotifier { } Future _attemptReconnect() async { - if (_lastSessionId == null || _lastPin == null) { + if (_lastSessionId == null || _lastPin == null || _lastHostAddress == null) { appLogger.w('CompanionRemote: No stored credentials for reconnect'); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost'); notifyListeners(); @@ -305,7 +312,7 @@ class CompanionRemoteProvider with ChangeNotifier { _peerService = CompanionRemotePeerService(); _setupPeerServiceListeners(); - await _peerService!.joinSession(_lastSessionId!, _lastPin!, _deviceName, _platform); + await _peerService!.joinSession(_lastSessionId!, _lastPin!, _deviceName, _platform, _lastHostAddress!); _session = _session?.copyWith(status: RemoteSessionStatus.connected); _reconnectAttempts = 0; @@ -330,7 +337,10 @@ class CompanionRemoteProvider with ChangeNotifier { void cancelReconnect() { _reconnectTimer?.cancel(); _reconnectAttempts = 0; - _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, connectedDevice: null); + _session = _session?.copyWith( + status: RemoteSessionStatus.disconnected, + clearConnectedDevice: true, + ); notifyListeners(); } @@ -479,6 +489,7 @@ class CompanionRemoteProvider with ChangeNotifier { deviceName: deviceToSave.name, platform: deviceToSave.platform, lastConnected: DateTime.now(), + hostAddress: _peerService?.hostAddress, ); if (_discoveryService != null) { @@ -488,7 +499,13 @@ class CompanionRemoteProvider with ChangeNotifier { /// Connect to a recent session Future 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 diff --git a/lib/screens/companion_remote/mobile_remote_screen.dart b/lib/screens/companion_remote/mobile_remote_screen.dart index 53f164dc..4d39db5d 100644 --- a/lib/screens/companion_remote/mobile_remote_screen.dart +++ b/lib/screens/companion_remote/mobile_remote_screen.dart @@ -35,14 +35,8 @@ class _MobileRemoteScreenState extends State { title: const Text('Disconnect'), content: const Text('Do you want to disconnect from the remote session?'), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Disconnect'), - ), + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Disconnect')), ], ), ); @@ -68,10 +62,7 @@ class _MobileRemoteScreenState extends State { children: [ const CircularProgressIndicator(), const SizedBox(height: 24), - Text( - 'Reconnecting...', - style: Theme.of(context).textTheme.titleLarge, - ), + Text('Reconnecting...', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 8), Text( 'Attempt ${provider.reconnectAttempts} of 5', @@ -81,15 +72,9 @@ class _MobileRemoteScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - OutlinedButton( - onPressed: () => provider.cancelReconnect(), - child: const Text('Cancel'), - ), + OutlinedButton(onPressed: () => provider.cancelReconnect(), child: const Text('Cancel')), const SizedBox(width: 16), - FilledButton( - onPressed: () => provider.retryReconnectNow(), - child: const Text('Retry Now'), - ), + FilledButton(onPressed: () => provider.retryReconnectNow(), child: const Text('Retry Now')), ], ), ], @@ -114,10 +99,7 @@ class _MobileRemoteScreenState extends State { const SizedBox(height: 32), FilledButton.icon( onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const PairingScreen()), - ); + Navigator.push(context, MaterialPageRoute(builder: (context) => const PairingScreen())); }, icon: const Icon(Icons.link), label: const Text('Connect to Device'), @@ -141,10 +123,7 @@ class _RemoteControlLayout extends StatelessWidget { Widget build(BuildContext context) { if (PlatformDetector.isDesktop(context)) { return Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: const _RemoteControlContent(), - ), + child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 400), child: const _RemoteControlContent()), ); } @@ -195,10 +174,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { color: Theme.of(context).colorScheme.primaryContainer, child: Row( children: [ - Icon( - Icons.computer, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), + Icon(Icons.computer, color: Theme.of(context).colorScheme.onPrimaryContainer), const SizedBox(width: 12), Expanded( child: Column( @@ -206,15 +182,15 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { children: [ Text( device.name, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer), ), Text( device.platform, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer), ), ], ), @@ -222,10 +198,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { Container( width: 8, height: 8, - decoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle), ), ], ), @@ -269,16 +242,8 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _RemoteButton( - icon: Icons.home, - label: 'Home', - onPressed: () => _sendCommand(RemoteCommandType.home), - ), - _RemoteButton( - icon: Icons.arrow_back, - label: 'Back', - onPressed: () => _sendCommand(RemoteCommandType.back), - ), + _RemoteButton(icon: Icons.home, label: 'Home', onPressed: () => _sendCommand(RemoteCommandType.home)), + _RemoteButton(icon: Icons.arrow_back, label: 'Back', onPressed: () => _sendCommand(RemoteCommandType.back)), _RemoteButton( icon: Icons.menu, label: 'Menu', @@ -287,14 +252,9 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { ], ), const SizedBox(height: 32), - Center( - child: _DPad(onCommand: _sendCommand), - ), + Center(child: _DPad(onCommand: _sendCommand)), const SizedBox(height: 32), - Text( - 'Tab Navigation', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('Tab Navigation', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 16), Wrap( spacing: 8, @@ -370,11 +330,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { onPressed: () => _sendCommand(RemoteCommandType.seekBackward), ), const SizedBox(width: 16), - _RemoteButton( - icon: Icons.stop, - label: 'Stop', - onPressed: () => _sendCommand(RemoteCommandType.stop), - ), + _RemoteButton(icon: Icons.stop, label: 'Stop', onPressed: () => _sendCommand(RemoteCommandType.stop)), const SizedBox(width: 16), _RemoteButton( icon: Icons.forward_10, @@ -384,10 +340,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { ], ), const SizedBox(height: 32), - Text( - 'Volume', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('Volume', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -424,11 +377,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { runSpacing: 12, alignment: WrapAlignment.center, children: [ - _RemoteCard( - icon: Icons.search, - label: 'Search', - onPressed: _showSearchSheet, - ), + _RemoteCard(icon: Icons.search, label: 'Search', onPressed: _showSearchSheet), _RemoteCard( icon: Icons.fullscreen, label: 'Fullscreen', @@ -583,19 +532,12 @@ class _RemoteButton extends StatelessWidget { HapticFeedback.lightImpact(); onPressed(); }, - style: FilledButton.styleFrom( - padding: EdgeInsets.zero, - shape: const CircleBorder(), - ), + style: FilledButton.styleFrom(padding: EdgeInsets.zero, shape: const CircleBorder()), child: Icon(icon, size: iconSize), ), ), const SizedBox(height: 4), - Text( - label, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ), + Text(label, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center), ], ); } @@ -606,11 +548,7 @@ class _RemoteChip extends StatelessWidget { final String label; final VoidCallback onPressed; - const _RemoteChip({ - required this.icon, - required this.label, - required this.onPressed, - }); + const _RemoteChip({required this.icon, required this.label, required this.onPressed}); @override Widget build(BuildContext context) { @@ -654,12 +592,7 @@ class _SearchBottomSheetState extends State<_SearchBottomSheet> { @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.viewInsetsOf(context).bottom, - left: 16, - right: 16, - top: 16, - ), + padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom, left: 16, right: 16, top: 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -669,13 +602,8 @@ class _SearchBottomSheetState extends State<_SearchBottomSheet> { decoration: InputDecoration( hintText: 'Search on desktop...', prefixIcon: const Icon(Icons.search), - suffixIcon: IconButton( - icon: const Icon(Icons.send), - onPressed: () => _submit(_controller.text), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(100), - ), + suffixIcon: IconButton(icon: const Icon(Icons.send), onPressed: () => _submit(_controller.text)), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(100)), ), onSubmitted: _submit, ), @@ -691,11 +619,7 @@ class _RemoteCard extends StatelessWidget { final String label; final VoidCallback onPressed; - const _RemoteCard({ - required this.icon, - required this.label, - required this.onPressed, - }); + const _RemoteCard({required this.icon, required this.label, required this.onPressed}); @override Widget build(BuildContext context) { @@ -714,11 +638,7 @@ class _RemoteCard extends StatelessWidget { children: [ Icon(icon, size: 32), const SizedBox(height: 8), - Text( - label, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ), + Text(label, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center), ], ), ), diff --git a/lib/screens/companion_remote/pairing_screen.dart b/lib/screens/companion_remote/pairing_screen.dart index 96bff012..d06956ff 100644 --- a/lib/screens/companion_remote/pairing_screen.dart +++ b/lib/screens/companion_remote/pairing_screen.dart @@ -17,6 +17,7 @@ class PairingScreen extends StatefulWidget { } class _PairingScreenState extends State { + final _hostAddressController = TextEditingController(); final _sessionIdController = TextEditingController(); final _pinController = TextEditingController(); final _formKey = GlobalKey(); @@ -44,6 +45,7 @@ class _PairingScreenState extends State { @override void dispose() { + _hostAddressController.dispose(); _sessionIdController.dispose(); _pinController.dispose(); _scannerController?.dispose(); @@ -105,7 +107,11 @@ class _PairingScreenState extends State { try { final provider = context.read(); - 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) { Navigator.of(context).pop(); @@ -133,26 +139,33 @@ class _PairingScreenState extends State { if (data == _lastScannedCode) return; _lastScannedCode = data; - final parts = data.split(':'); - if (parts.length == 2) { + // New format: ip|port|sessionId|pin (4 parts separated by pipe) + 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(); setState(() { _errorMessage = null; _isConnecting = true; }); // Connect directly instead of going through _connect() which requires Form validation - _connectWithCredentials(parts[0], parts[1]); + _connectWithCredentials(sessionId, pin, hostAddress); } else { setState(() { - _errorMessage = 'Invalid QR code format'; + _errorMessage = 'Invalid QR code format - expected 4 parts (ip|port|sessionId|pin)'; }); } } - Future _connectWithCredentials(String sessionId, String pin) async { + Future _connectWithCredentials(String sessionId, String pin, String hostAddress) async { try { final provider = context.read(); - await provider.joinSession(sessionId.trim().toUpperCase(), pin.trim()); + await provider.joinSession(sessionId.trim().toUpperCase(), pin.trim(), hostAddress.trim()); if (mounted) { Navigator.of(context).pop(); @@ -472,6 +485,33 @@ class _PairingScreenState extends State { ), 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( controller: _sessionIdController, decoration: InputDecoration( diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index c2c326fc..beb6874b 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -1129,7 +1129,9 @@ class _DiscoverScreenState extends State onKeyEvent: isDesktop ? _handleCompanionRemoteKeyEvent : null, child: Container( 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), ), child: Stack( @@ -1144,10 +1146,7 @@ class _DiscoverScreenState extends State if (isDesktop) { RemoteSessionDialog.show(context); } else { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => MobileRemoteScreen()), - ); + Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen())); } }, tooltip: 'Companion Remote', @@ -1372,12 +1371,7 @@ class _DiscoverScreenState extends State // Overlaid app bar — excluded from default focus traversal so that // initial/tab-switch focus lands on content (hero/hubs), not the toolbar. // Toolbar buttons are still reachable via explicit UP from hero section. - Positioned( - top: 0, - left: 0, - right: 0, - child: ExcludeFocusTraversal(child: _buildOverlaidAppBar()), - ), + Positioned(top: 0, left: 0, right: 0, child: ExcludeFocusTraversal(child: _buildOverlaidAppBar())), ], ), ); diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 14f4de84..cee45320 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -816,10 +816,7 @@ class _SettingsScreenState extends State with FocusableTab { : const Text('Control a desktop device'), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const MobileRemoteScreen()), - ); + Navigator.push(context, MaterialPageRoute(builder: (context) => const MobileRemoteScreen())); }, ), ], diff --git a/lib/services/companion_remote/companion_remote_peer_service.dart b/lib/services/companion_remote/companion_remote_peer_service.dart index 72dfccbd..697d9649 100644 --- a/lib/services/companion_remote/companion_remote_peer_service.dart +++ b/lib/services/companion_remote/companion_remote_peer_service.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; 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_type.dart'; @@ -15,6 +17,8 @@ enum RemotePeerErrorType { serverError, timeout, invalidSession, + authFailed, + networkError, unknown, } @@ -23,22 +27,24 @@ class RemotePeerError { final String message; final dynamic originalError; - const RemotePeerError({ - required this.type, - required this.message, - this.originalError, - }); + const RemotePeerError({required this.type, required this.message, this.originalError}); @override String toString() => 'RemotePeerError($type): $message'; } class CompanionRemotePeerService { - Peer? _peer; - DataConnection? _connection; + // Server-side (host) fields + HttpServer? _server; + WebSocket? _clientSocket; + + // Client-side (remote) fields + IOWebSocketChannel? _channel; + String? _sessionId; String? _pin; String? _myPeerId; + String? _hostAddress; // Format: "ip:port" RemoteSessionRole? _role; final _commandReceivedController = StreamController.broadcast(); @@ -47,9 +53,6 @@ class CompanionRemotePeerService { final _errorController = StreamController.broadcast(); final _connectionStateController = StreamController.broadcast(); - int _reconnectAttempts = 0; - static const int _maxReconnectAttempts = 3; - Timer? _reconnectTimer; Timer? _pingTimer; Stream get onCommandReceived => _commandReceivedController.stream; @@ -61,9 +64,10 @@ class CompanionRemotePeerService { String? get sessionId => _sessionId; String? get pin => _pin; String? get myPeerId => _myPeerId; + String? get hostAddress => _hostAddress; RemoteSessionRole? get role => _role; bool get isHost => _role == RemoteSessionRole.host; - bool get isConnected => _connection != null; + bool get isConnected => _clientSocket != null || (_channel != null && _channel?.closeCode == null); String _generateSessionId() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; @@ -76,263 +80,368 @@ class CompanionRemotePeerService { return List.generate(6, (index) => random.nextInt(10).toString()).join(); } - void _attachCommonPeerListeners({ - required Completer completer, - required RemotePeerErrorType errorType, - required String errorMessage, - }) { - _peer!.on('disconnected').listen((_) { - appLogger.w('CompanionRemote: Peer disconnected from server'); - _handleDisconnectedFromServer(); - }); + Future _getLocalIpAddress() async { + try { + final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4); - _peer!.on('close').listen((_) { - appLogger.d('CompanionRemote: Peer closed'); - _connectionStateController.add(RemoteSessionStatus.disconnected); - }); + // Prefer WiFi interface, then any non-loopback + for (final interface in interfaces) { + // Skip loopback + if (interface.name.toLowerCase().contains('lo')) continue; - _peer!.on('error').listen((error) { - appLogger.e('CompanionRemote: Peer error', error: error); - _errorController.add( - RemotePeerError( - type: errorType, - message: '$errorMessage: $error', - originalError: error, - ), - ); - if (!completer.isCompleted) { - completer.completeError(error); + for (final addr in interface.addresses) { + if (!addr.isLoopback && addr.type == InternetAddressType.IPv4) { + // Prefer names that suggest WiFi/Ethernet + if (interface.name.toLowerCase().contains('en') || + interface.name.toLowerCase().contains('wl') || + interface.name.toLowerCase().contains('eth')) { + return addr.address; + } + } + } } - }); + + // 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 { - if (_peer != null) { + Future<({String sessionId, String pin, String address})> createSession(String deviceName, String platform) async { + if (_server != null) { await disconnect(); } _role = RemoteSessionRole.host; _sessionId = _generateSessionId(); _pin = _generatePin(); - _reconnectAttempts = 0; - - final completer = Completer<({String sessionId, String pin})>(); + _myPeerId = 'host-$_sessionId'; 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) { - _myPeerId = id as String; - appLogger.d('CompanionRemote: Host peer opened with ID: $_myPeerId'); - _connectionStateController.add(RemoteSessionStatus.connected); - if (!completer.isCompleted) { - completer.complete((sessionId: _sessionId!, pin: _pin!)); + try { + _server = await HttpServer.bind(InternetAddress.anyIPv4, preferredPort); + appLogger.d('CompanionRemote: Server bound to port $preferredPort'); + } catch (e) { + appLogger.w('CompanionRemote: Port $preferredPort occupied, using random port'); + _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) { - final dataConn = conn as DataConnection; - _handleNewConnection(dataConn, deviceName, platform); - }); + _connectionStateController.add(RemoteSessionStatus.connected); - _attachCommonPeerListeners( - completer: completer, - errorType: RemotePeerErrorType.serverError, - errorMessage: 'Server error', - ); + return (sessionId: _sessionId!, pin: _pin!, address: _hostAddress!); } catch (e) { - appLogger.e('CompanionRemote: Failed to create peer', error: e); - if (!completer.isCompleted) { - completer.completeError(e); - } + appLogger.e('CompanionRemote: Failed to create server', error: e); + _errorController.add( + RemotePeerError( + type: RemotePeerErrorType.serverError, + message: 'Failed to create server: $e', + originalError: e, + ), + ); + rethrow; } + } - return completer.future.timeout( - const Duration(seconds: 10), - onTimeout: () { - throw RemotePeerError( - type: RemotePeerErrorType.timeout, - message: 'Timed out creating session', + void _handleNewWebSocketConnection(WebSocket socket, String hostDeviceName, String hostPlatform) { + appLogger.d('CompanionRemote: New WebSocket connection'); + + bool isAuthenticated = false; + Timer? authTimeout; + + // 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; + + 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 joinSession( - String sessionId, - String pin, - String deviceName, - String platform, - ) async { - if (_peer != null) { + Future joinSession(String sessionId, String pin, String deviceName, String platform, String hostAddress) async { + if (_channel != null) { await disconnect(); } _role = RemoteSessionRole.remote; _sessionId = sessionId.toUpperCase(); _pin = pin; - _reconnectAttempts = 0; + _hostAddress = hostAddress; + _myPeerId = 'remote-${Random().nextInt(99999)}'; final completer = Completer(); try { - _peer = Peer(); + final url = 'ws://$hostAddress/ws'; + appLogger.d('CompanionRemote: Connecting to $url'); - _peer!.on('open').listen((id) { - _myPeerId = id as String; - appLogger.d('CompanionRemote: Remote peer opened with ID: $_myPeerId'); + _connectionStateController.add(RemoteSessionStatus.connecting); - final hostPeerId = 'cr-$_sessionId-$_pin'; - appLogger.d('CompanionRemote: Connecting to host: $hostPeerId'); + _channel = IOWebSocketChannel.connect(Uri.parse(url)); - _connectionStateController.add(RemoteSessionStatus.connecting); - - final conn = _peer!.connect(hostPeerId, options: PeerConnectOption(reliable: true)); - _handleNewConnection(conn, deviceName, platform, isOutgoing: true, completer: completer); + // Send authentication message + final authMessage = jsonEncode({ + 'type': 'auth', + 'sessionId': _sessionId, + 'pin': _pin, + 'deviceName': deviceName, + 'platform': platform, }); + _channel!.sink.add(authMessage); - _attachCommonPeerListeners( - completer: completer, - errorType: RemotePeerErrorType.connectionFailed, - errorMessage: 'Failed to connect to session', + // Listen for messages + _channel!.stream.listen( + (data) { + try { + final json = jsonDecode(data as String) as Map; + 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) { - appLogger.e('CompanionRemote: Failed to create peer for joining', error: e); + appLogger.e('CompanionRemote: Failed to connect', error: e); + if (!completer.isCompleted) { completer.completeError(e); } + + _errorController.add( + RemotePeerError(type: RemotePeerErrorType.connectionFailed, message: 'Failed to connect: $e', originalError: e), + ); } return completer.future.timeout( const Duration(seconds: 15), - onTimeout: () { - throw RemotePeerError( - type: RemotePeerErrorType.timeout, - message: 'Timed out joining session', - ); + onTimeout: () async { + // Clean up channel on timeout + if (_channel != null) { + 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? 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; - 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() { _stopPingTimer(); _pingTimer = Timer.periodic(const Duration(seconds: 5), (_) { - if (_connection != null) { - sendCommand(RemoteCommand( - type: RemoteCommandType.ping, - deviceId: _myPeerId ?? 'unknown', - deviceName: 'local', - )); + if (isConnected) { + sendCommand(RemoteCommand(type: RemoteCommandType.ping, deviceId: _myPeerId ?? 'unknown', deviceName: 'local')); } }); } @@ -343,36 +452,40 @@ class CompanionRemotePeerService { } void _sendPong(String deviceName, String platform) { - sendCommand(RemoteCommand( - type: RemoteCommandType.pong, - deviceId: _myPeerId ?? 'unknown', - deviceName: deviceName, - data: {'platform': platform}, - )); + sendCommand( + RemoteCommand( + type: RemoteCommandType.pong, + deviceId: _myPeerId ?? 'unknown', + deviceName: deviceName, + data: {'platform': platform}, + ), + ); } void sendDeviceInfo(String deviceName, String platform) { - sendCommand(RemoteCommand( - type: RemoteCommandType.deviceInfo, - deviceId: _myPeerId ?? 'unknown', - deviceName: deviceName, - data: { - 'platform': platform, - 'role': _role?.name, - }, - )); + sendCommand( + RemoteCommand( + type: RemoteCommandType.deviceInfo, + deviceId: _myPeerId ?? 'unknown', + deviceName: deviceName, + data: {'platform': platform, 'role': _role?.name}, + ), + ); } void sendCommand(RemoteCommand command) { - if (_connection == null) { - appLogger.w('CompanionRemote: No connection to send command'); - return; - } - try { - final json = command.toJson(); - _connection!.send(json); - appLogger.d('CompanionRemote: Sent command: ${command.type}'); + final json = jsonEncode(command.toJson()); + + 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) { appLogger.e('CompanionRemote: Failed to send command', error: e); _errorController.add( @@ -389,29 +502,37 @@ class CompanionRemotePeerService { appLogger.d('CompanionRemote: Disconnecting'); _stopPingTimer(); - _reconnectTimer?.cancel(); - _connection?.close(); - _connection = null; + if (_clientSocket != null) { + await _clientSocket!.close(); + _clientSocket = null; + } - _peer?.dispose(); - _peer = null; + if (_channel != null) { + await _channel!.sink.close(); + _channel = null; + } + + if (_server != null) { + await _server!.close(); + _server = null; + } _sessionId = null; _pin = null; _myPeerId = null; + _hostAddress = null; _role = null; - _reconnectAttempts = 0; _connectionStateController.add(RemoteSessionStatus.disconnected); } - void dispose() { - disconnect(); - _commandReceivedController.close(); - _deviceConnectedController.close(); - _deviceDisconnectedController.close(); - _errorController.close(); - _connectionStateController.close(); + Future dispose() async { + await disconnect(); + await _commandReceivedController.close(); + await _deviceConnectedController.close(); + await _deviceDisconnectedController.close(); + await _errorController.close(); + await _connectionStateController.close(); } } diff --git a/lib/services/gamepad_service.dart b/lib/services/gamepad_service.dart index 78fe94c8..27efd8ea 100644 --- a/lib/services/gamepad_service.dart +++ b/lib/services/gamepad_service.dart @@ -343,5 +343,4 @@ class GamepadService { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; } } - } diff --git a/lib/widgets/companion_remote/remote_session_dialog.dart b/lib/widgets/companion_remote/remote_session_dialog.dart index 08f2b027..7fcc0bd4 100644 --- a/lib/widgets/companion_remote/remote_session_dialog.dart +++ b/lib/widgets/companion_remote/remote_session_dialog.dart @@ -24,6 +24,7 @@ class RemoteSessionDialog extends StatefulWidget { class _RemoteSessionDialogState extends State { bool _isCreatingSession = false; String? _errorMessage; + String? _hostAddress; // Format: "ip:port" @override void initState() { @@ -35,14 +36,16 @@ class _RemoteSessionDialogState extends State { setState(() { _isCreatingSession = true; _errorMessage = null; + _hostAddress = null; }); try { final provider = context.read(); - await provider.createSession(); + final result = await provider.createSession(); setState(() { _isCreatingSession = false; + _hostAddress = result.address; }); } catch (e) { appLogger.e('Failed to create companion remote session', error: e); @@ -55,12 +58,9 @@ class _RemoteSessionDialogState extends State { void _copyToClipboard(String text, String label) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('$label copied to clipboard'), - duration: const Duration(seconds: 2), - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('$label copied to clipboard'), duration: const Duration(seconds: 2))); } @override @@ -76,10 +76,7 @@ class _RemoteSessionDialogState extends State { children: [ const CircularProgressIndicator(), const SizedBox(height: 16), - Text( - 'Creating remote session...', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('Creating remote session...', style: Theme.of(context).textTheme.titleMedium), ], ), ), @@ -95,40 +92,32 @@ class _RemoteSessionDialogState extends State { children: [ const Text('Failed to create remote session:'), const SizedBox(height: 8), - Text( - _errorMessage!, - style: const TextStyle(fontFamily: 'monospace'), - ), + Text(_errorMessage!, style: const TextStyle(fontFamily: 'monospace')), ], ), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ), - TextButton( - onPressed: _createSession, - child: const Text('Retry'), - ), + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Close')), + TextButton(onPressed: _createSession, child: const Text('Retry')), ], ); } final session = provider.session; - if (session == null) { + if (session == null || _hostAddress == null) { return AlertDialog( title: const Text('Error'), content: const Text('No session available'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ), - ], + actions: [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( child: ConstrainedBox( @@ -147,45 +136,32 @@ class _RemoteSessionDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Companion Remote', - style: Theme.of(context).textTheme.headlineSmall, - ), + Text('Companion Remote', style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 4), Text( session.connectedDevice != null ? 'Connected to ${session.connectedDevice!.name}' : 'Waiting for connection...', style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: session.connectedDevice != null - ? Colors.green - : Theme.of(context).textTheme.bodySmall?.color, - ), + color: session.connectedDevice != null + ? Colors.green + : Theme.of(context).textTheme.bodySmall?.color, + ), ), ], ), ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), + IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()), ], ), const SizedBox(height: 24), if (session.connectedDevice == null) ...[ - Text( - 'Scan QR Code', - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), + Text('Scan QR Code', style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center), const SizedBox(height: 16), Center( child: Container( padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: QrImageView( data: qrData, version: QrVersions.auto, @@ -203,6 +179,13 @@ class _RemoteSessionDialogState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 16), + _buildCodeCard( + context, + 'Host Address', + _hostAddress!, + onCopy: () => _copyToClipboard(_hostAddress!, 'Host Address'), + ), + const SizedBox(height: 12), _buildCodeCard( context, 'Session ID', @@ -210,37 +193,19 @@ class _RemoteSessionDialogState extends State { onCopy: () => _copyToClipboard(session.sessionId, 'Session ID'), ), const SizedBox(height: 12), - _buildCodeCard( - context, - 'PIN', - session.pin, - onCopy: () => _copyToClipboard(session.pin, 'PIN'), - ), + _buildCodeCard(context, 'PIN', session.pin, onCopy: () => _copyToClipboard(session.pin, 'PIN')), ] else ...[ Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ - const Icon( - Icons.check_circle, - color: Colors.green, - size: 48, - ), + const Icon(Icons.check_circle, color: Colors.green, size: 48), const SizedBox(height: 8), - Text( - 'Connected', - style: Theme.of(context).textTheme.titleLarge, - ), + Text('Connected', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 8), - Text( - session.connectedDevice!.name, - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - session.connectedDevice!.platform, - style: Theme.of(context).textTheme.bodySmall, - ), + Text(session.connectedDevice!.name, style: Theme.of(context).textTheme.bodyLarge), + Text(session.connectedDevice!.platform, style: Theme.of(context).textTheme.bodySmall), ], ), ), @@ -276,10 +241,7 @@ class _RemoteSessionDialogState extends State { child: const Text('Disconnect'), ), const SizedBox(width: 8), - FilledButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Minimize'), - ), + FilledButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Minimize')), ], ), ], @@ -291,12 +253,7 @@ class _RemoteSessionDialogState extends State { ); } - Widget _buildCodeCard( - BuildContext context, - String label, - String code, { - VoidCallback? onCopy, - }) { + Widget _buildCodeCard(BuildContext context, String label, String code, {VoidCallback? onCopy}) { return Card( child: Padding( padding: const EdgeInsets.all(16.0), @@ -306,26 +263,16 @@ class _RemoteSessionDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: Theme.of(context).textTheme.bodySmall, - ), + Text(label, style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: 4), Text( code, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontFamily: 'monospace', - letterSpacing: 2, - ), + style: Theme.of(context).textTheme.titleLarge?.copyWith(fontFamily: 'monospace', letterSpacing: 2), ), ], ), ), - IconButton( - icon: const Icon(Icons.copy), - onPressed: onCopy, - tooltip: 'Copy to clipboard', - ), + IconButton(icon: const Icon(Icons.copy), onPressed: onCopy, tooltip: 'Copy to clipboard'), ], ), ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 69e7208d..0a25aa35 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,7 +6,6 @@ #include "generated_plugin_registrant.h" -#include #include #include #include @@ -15,9 +14,6 @@ #include 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 = fl_plugin_registry_get_registrar_for_plugin(registry, "OsMediaControlsPlugin"); os_media_controls_plugin_register_with_registrar(os_media_controls_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index a560d685..eb699977 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - flutter_webrtc os_media_controls screen_retriever_linux sqlite3_flutter_libs diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index dc700d78..718fe9a5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import connectivity_plus import device_info_plus import file_picker -import flutter_webrtc import in_app_review import mobile_scanner import os_media_controls @@ -27,7 +26,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) - FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index db8f2cab..d0b49723 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,9 +5,6 @@ PODS: - FlutterMacOS - file_picker (0.0.1): - FlutterMacOS - - flutter_webrtc (1.2.0): - - FlutterMacOS - - WebRTC-SDK (= 137.7151.04) - FlutterMacOS (1.0.0) - in_app_review (2.0.0): - FlutterMacOS @@ -59,7 +56,6 @@ PODS: - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS - - WebRTC-SDK (137.7151.04) - window_manager (0.5.0): - FlutterMacOS @@ -67,7 +63,6 @@ DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/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`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) @@ -86,7 +81,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - sqlite3 - - WebRTC-SDK EXTERNAL SOURCES: connectivity_plus: @@ -95,8 +89,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos - flutter_webrtc: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos FlutterMacOS: :path: Flutter/ephemeral in_app_review: @@ -130,7 +122,6 @@ SPEC CHECKSUMS: connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a - flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4 mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 @@ -145,7 +136,6 @@ SPEC CHECKSUMS: universal_gamepad: 8922f1f238f62d6847de887228976d5b572b57da url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b - WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e window_manager: b729e31d38fb04905235df9ea896128991cad99e PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d diff --git a/pubspec.lock b/pubspec.lock index 442ce73b..0a9efc48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -297,14 +297,6 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -369,14 +361,6 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -493,14 +477,6 @@ packages: description: flutter source: sdk 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: dependency: transitive description: @@ -838,14 +814,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - peerdart: - dependency: "direct main" - description: - name: peerdart - sha256: "1d0db041d42194f42e57d8889d029fb8d107610bf1062b39f43f4651de547e3c" - url: "https://pub.dev" - source: hosted - version: "0.5.6" petitparser: dependency: transitive description: @@ -1451,14 +1419,6 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4c939f30..39a4a1f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,10 +49,6 @@ dependencies: dart_discord_presence: ^1.1.0 flutter_svg: ^2.2.3 mobile_scanner: ^6.0.2 - peerdart: ^0.5.6 - -dependency_overrides: - flutter_webrtc: ^1.3.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 88e1c798..bc3ebff2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -18,8 +17,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); - FlutterWebRTCPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); OsMediaControlsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1dca6fac..db77f229 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus - flutter_webrtc os_media_controls screen_retriever_windows sqlite3_flutter_libs From 660267b504cab5bad9879d76b06adc3c9e8b6e07 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:02:22 +0100 Subject: [PATCH 5/8] fix: companion remote discovery leak, reconnect, and auth hardening - Dispose old discovery service before creating new one in loadRecentSessions() - Host waits for client reconnect instead of calling joinSession - Clear stale error messages on successful reconnect - Rate limit auth attempts (5 tries, 30s lockout) - Use Random.secure() for peer ID - Extract shared ACK helper methods --- lib/providers/companion_remote_provider.dart | 20 +++++- .../companion_remote_peer_service.dart | 69 ++++++++++++------- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/lib/providers/companion_remote_provider.dart b/lib/providers/companion_remote_provider.dart index 01416abc..0adf1f97 100644 --- a/lib/providers/companion_remote_provider.dart +++ b/lib/providers/companion_remote_provider.dart @@ -45,6 +45,7 @@ class CompanionRemoteProvider with ChangeNotifier { StreamSubscription? _deviceDisconnectedSubscription; StreamSubscription? _errorSubscription; StreamSubscription? _statusSubscription; + StreamSubscription>? _recentSessionsSubscription; CommandReceivedCallback? onCommandReceived; DeviceApprovalCallback? onDeviceApprovalRequired; @@ -136,6 +137,15 @@ class CompanionRemoteProvider with ChangeNotifier { clearConnectedDevice: true, ); notifyListeners(); + } else if (isHost) { + // Host keeps the server running — the client will reconnect on its own + _session = _session?.copyWith( + status: RemoteSessionStatus.reconnecting, + clearConnectedDevice: true, + clearErrorMessage: true, + ); + notifyListeners(); + appLogger.d('CompanionRemote: Host waiting for client to reconnect'); } else { _session = _session?.copyWith(status: RemoteSessionStatus.reconnecting); notifyListeners(); @@ -314,7 +324,7 @@ class CompanionRemoteProvider with ChangeNotifier { await _peerService!.joinSession(_lastSessionId!, _lastPin!, _deviceName, _platform, _lastHostAddress!); - _session = _session?.copyWith(status: RemoteSessionStatus.connected); + _session = _session?.copyWith(status: RemoteSessionStatus.connected, clearErrorMessage: true); _reconnectAttempts = 0; notifyListeners(); appLogger.d('CompanionRemote: Reconnected successfully'); @@ -451,10 +461,15 @@ class CompanionRemoteProvider with ChangeNotifier { /// Load recent sessions Future loadRecentSessions() async { try { + // Dispose previous discovery service and subscription to avoid leaks + _recentSessionsSubscription?.cancel(); + _recentSessionsSubscription = null; + _discoveryService?.dispose(); + _discoveryService = CompanionRemoteDiscoveryService(); // Listen for recent sessions updates - _discoveryService!.recentSessions.listen((sessions) { + _recentSessionsSubscription = _discoveryService!.recentSessions.listen((sessions) { _recentSessions.clear(); _recentSessions.addAll(sessions); notifyListeners(); @@ -526,6 +541,7 @@ class CompanionRemoteProvider with ChangeNotifier { void dispose() { _reconnectTimer?.cancel(); leaveSession(); + _recentSessionsSubscription?.cancel(); _discoveryService?.dispose(); super.dispose(); } diff --git a/lib/services/companion_remote/companion_remote_peer_service.dart b/lib/services/companion_remote/companion_remote_peer_service.dart index 697d9649..e91684ed 100644 --- a/lib/services/companion_remote/companion_remote_peer_service.dart +++ b/lib/services/companion_remote/companion_remote_peer_service.dart @@ -55,6 +55,12 @@ class CompanionRemotePeerService { Timer? _pingTimer; + // Auth rate limiting + int _failedAuthAttempts = 0; + DateTime? _authLockoutUntil; + static const int _maxFailedAuthAttempts = 5; + static const Duration _authLockoutDuration = Duration(seconds: 30); + Stream get onCommandReceived => _commandReceivedController.stream; Stream get onDeviceConnected => _deviceConnectedController.stream; Stream get onDeviceDisconnected => _deviceDisconnectedController.stream; @@ -198,12 +204,21 @@ class CompanionRemotePeerService { if (!isAuthenticated) { // First message must be authentication if (json['type'] == 'auth') { + // Rate limiting: reject if locked out + if (_authLockoutUntil != null && DateTime.now().isBefore(_authLockoutUntil!)) { + appLogger.w('CompanionRemote: Auth attempt rejected (rate limited)'); + socket.add(jsonEncode({'type': 'authFailed', 'message': 'Too many attempts. Try again later.'})); + socket.close(4005, 'Rate limited'); + return; + } + 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) { + _failedAuthAttempts = 0; isAuthenticated = true; authTimeout?.cancel(); @@ -234,7 +249,12 @@ class CompanionRemotePeerService { // Note: Client sends keepalive pings, host only responds with pongs } else { - appLogger.w('CompanionRemote: Invalid credentials'); + _failedAuthAttempts++; + if (_failedAuthAttempts >= _maxFailedAuthAttempts) { + _authLockoutUntil = DateTime.now().add(_authLockoutDuration); + appLogger.w('CompanionRemote: Too many failed auth attempts, locked out for ${_authLockoutDuration.inSeconds}s'); + } + appLogger.w('CompanionRemote: Invalid credentials (attempt $_failedAuthAttempts/$_maxFailedAuthAttempts)'); socket.add(jsonEncode({'type': 'authFailed', 'message': 'Invalid session ID or PIN'})); socket.close(4003, 'Invalid credentials'); } @@ -247,18 +267,8 @@ class CompanionRemotePeerService { 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())); + if (_shouldSendAck(command)) { + _sendAck(command, hostDeviceName); } _commandReceivedController.add(command); @@ -304,7 +314,7 @@ class CompanionRemotePeerService { _sessionId = sessionId.toUpperCase(); _pin = pin; _hostAddress = hostAddress; - _myPeerId = 'remote-${Random().nextInt(99999)}'; + _myPeerId = 'remote-${Random.secure().nextInt(99999)}'; final completer = Completer(); @@ -364,18 +374,8 @@ class CompanionRemotePeerService { 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())); + if (_shouldSendAck(command)) { + _sendAck(command, deviceName); } _commandReceivedController.add(command); @@ -451,6 +451,23 @@ class CompanionRemotePeerService { _pingTimer = null; } + bool _shouldSendAck(RemoteCommand command) { + return command.type != RemoteCommandType.ping && + command.type != RemoteCommandType.pong && + command.type != RemoteCommandType.ack && + 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 _sendPong(String deviceName, String platform) { sendCommand( RemoteCommand( From 5c3fbfdff8e7abf15a0d1bebe2b7044bd4a52912 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:17:55 +0100 Subject: [PATCH 6/8] fix: remote dialog issues and optimize command protocol --- .../companion_remote/remote_command.dart | 55 ++----- .../companion_remote/remote_command.g.dart | 67 --------- .../companion_remote/remote_command_type.dart | 141 +----------------- lib/providers/companion_remote_provider.dart | 15 +- .../companion_remote_peer_service.dart | 35 ++--- .../companion_remote_receiver.dart | 2 - .../remote_session_dialog.dart | 5 +- 7 files changed, 30 insertions(+), 290 deletions(-) delete mode 100644 lib/models/companion_remote/remote_command.g.dart diff --git a/lib/models/companion_remote/remote_command.dart b/lib/models/companion_remote/remote_command.dart index b9c16726..d301da2c 100644 --- a/lib/models/companion_remote/remote_command.dart +++ b/lib/models/companion_remote/remote_command.dart @@ -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? 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 json) => _$RemoteCommandFromJson(json); - - Map toJson() => _$RemoteCommandToJson(this); - - RemoteCommand copyWith({ - RemoteCommandType? type, - String? deviceId, - String? deviceName, - DateTime? timestamp, - Map? data, - }) { + factory RemoteCommand.fromJson(Map 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?, ); } - @override - String toString() { - return 'RemoteCommand(type: ${type.name}, device: $deviceName, data: $data)'; + Map 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)'; } diff --git a/lib/models/companion_remote/remote_command.g.dart b/lib/models/companion_remote/remote_command.g.dart deleted file mode 100644 index db7cab90..00000000 --- a/lib/models/companion_remote/remote_command.g.dart +++ /dev/null @@ -1,67 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'remote_command.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -RemoteCommand _$RemoteCommandFromJson(Map 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?, -); - -Map _$RemoteCommandToJson(RemoteCommand instance) => { - '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', -}; diff --git a/lib/models/companion_remote/remote_command_type.dart b/lib/models/companion_remote/remote_command_type.dart index ca5febb1..dcf0f3d5 100644 --- a/lib/models/companion_remote/remote_command_type.dart +++ b/lib/models/companion_remote/remote_command_type.dart @@ -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, } diff --git a/lib/providers/companion_remote_provider.dart b/lib/providers/companion_remote_provider.dart index 0adf1f97..1879874b 100644 --- a/lib/providers/companion_remote_provider.dart +++ b/lib/providers/companion_remote_provider.dart @@ -168,12 +168,14 @@ class CompanionRemoteProvider with ChangeNotifier { Future _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() { diff --git a/lib/services/companion_remote/companion_remote_peer_service.dart b/lib/services/companion_remote/companion_remote_peer_service.dart index e91684ed..40dc6817 100644 --- a/lib/services/companion_remote/companion_remote_peer_service.dart +++ b/lib/services/companion_remote/companion_remote_peer_service.dart @@ -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}, ), ); } diff --git a/lib/services/companion_remote/companion_remote_receiver.dart b/lib/services/companion_remote/companion_remote_receiver.dart index fd2caee2..e9d16c78 100644 --- a/lib/services/companion_remote/companion_remote_receiver.dart +++ b/lib/services/companion_remote/companion_remote_receiver.dart @@ -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; diff --git a/lib/widgets/companion_remote/remote_session_dialog.dart b/lib/widgets/companion_remote/remote_session_dialog.dart index 7fcc0bd4..59c22497 100644 --- a/lib/widgets/companion_remote/remote_session_dialog.dart +++ b/lib/widgets/companion_remote/remote_session_dialog.dart @@ -29,7 +29,8 @@ class _RemoteSessionDialogState extends State { @override void initState() { super.initState(); - _createSession(); + // Defer to avoid notifyListeners() during build phase + WidgetsBinding.instance.addPostFrameCallback((_) => _createSession()); } Future _createSession() async { @@ -122,7 +123,7 @@ class _RemoteSessionDialogState extends State { return Dialog( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 500), - child: Padding( + child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, From d1af24e0a7751f272d08ae493e6ce9a8ec98137e Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:32:00 +0100 Subject: [PATCH 7/8] feat: player-aware companion remote with syncState, track cycling --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Podfile.lock | 77 +++++++++- ios/Runner.xcodeproj/project.pbxproj | 30 +++- .../companion_remote/remote_command_type.dart | 1 + lib/providers/companion_remote_provider.dart | 13 ++ .../mobile_remote_screen.dart | 131 ++++++++++-------- lib/screens/video_player_screen.dart | 126 ++++++++++++++--- .../companion_remote_receiver.dart | 13 +- .../video_controls/video_controls.dart | 8 +- 10 files changed, 316 insertions(+), 87 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..41bb3f8b 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 13.0 + 15.5 diff --git a/ios/Podfile b/ios/Podfile index e72b0638..a154672b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '15.5' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ebe46af4..4804c94b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,8 +6,56 @@ PODS: - file_picker (0.0.1): - Flutter - Flutter (1.0.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMLKit/BarcodeScanning (7.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 6.0.0) + - GoogleMLKit/MLKitCore (7.0.0): + - MLKitCommon (~> 12.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (3.5.0) - in_app_review (2.0.0): - Flutter + - MLImage (1.0.0-beta6) + - MLKitBarcodeScanning (6.0.0): + - MLKitCommon (~> 12.0) + - MLKitVision (~> 8.0) + - MLKitCommon (12.0.0): + - GoogleDataTransport (~> 10.0) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/Logger (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (8.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta6) + - MLKitCommon (~> 12.0) + - mobile_scanner (6.0.2): + - Flutter + - GoogleMLKit/BarcodeScanning (~> 7.0.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - os_media_controls (0.0.1): - Flutter - package_info_plus (0.4.5): @@ -15,6 +63,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - PromisesObjC (2.4.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -61,6 +110,7 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - os_media_controls (from `.symlinks/plugins/os_media_controls/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -74,6 +124,17 @@ DEPENDENCIES: SPEC REPOS: trunk: + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC - sqlite3 EXTERNAL SOURCES: @@ -87,6 +148,8 @@ EXTERNAL SOURCES: :path: Flutter in_app_review: :path: ".symlinks/plugins/in_app_review/ios" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/ios" os_media_controls: :path: ".symlinks/plugins/os_media_controls/ios" package_info_plus: @@ -113,10 +176,22 @@ SPEC CHECKSUMS: device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937 + MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 + MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 + MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d + MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e + mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b @@ -126,6 +201,6 @@ SPEC CHECKSUMS: wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 -PODFILE CHECKSUM: faeab82d8b6e3c9d039ddf62b7666988a3dea540 +PODFILE CHECKSUM: 16bb7f67e16d8aaa2bfb97666803fbfb291b6559 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 65cff4e7..37b7b3cc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -214,6 +214,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 2E10BAE564A4AFFCFF5B4EF0 /* [CP] Embed Pods Frameworks */, + BE3E47DA555D39F032C6A1DB /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -380,6 +381,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + BE3E47DA555D39F032C6A1DB /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -475,7 +493,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -496,7 +514,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Plezy; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -609,7 +627,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -660,7 +678,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -683,7 +701,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Plezy; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -710,7 +728,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Plezy; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/lib/models/companion_remote/remote_command_type.dart b/lib/models/companion_remote/remote_command_type.dart index dcf0f3d5..d7285ca1 100644 --- a/lib/models/companion_remote/remote_command_type.dart +++ b/lib/models/companion_remote/remote_command_type.dart @@ -49,4 +49,5 @@ enum RemoteCommandType { deviceInfo, disconnect, ack, + syncState, } diff --git a/lib/providers/companion_remote_provider.dart b/lib/providers/companion_remote_provider.dart index 1879874b..8d2f093e 100644 --- a/lib/providers/companion_remote_provider.dart +++ b/lib/providers/companion_remote_provider.dart @@ -26,6 +26,7 @@ class CompanionRemoteProvider with ChangeNotifier { String _platform = 'unknown'; final List _trustedDevices = []; final List _recentSessions = []; + bool _isPlayerActive = false; static const String _storageKey = 'companion_remote_trusted_devices'; static const String _lastDeviceKey = 'companion_remote_last_device'; @@ -61,6 +62,7 @@ class CompanionRemoteProvider with ChangeNotifier { RemoteDevice? get connectedDevice => _session?.connectedDevice; List get trustedDevices => List.unmodifiable(_trustedDevices); List get recentSessions => List.unmodifiable(_recentSessions); + bool get isPlayerActive => _isPlayerActive; CompanionRemoteProvider() { _initializeDeviceInfo(); @@ -108,6 +110,8 @@ class CompanionRemoteProvider with ChangeNotifier { if (command.type == RemoteCommandType.deviceInfo) { _handleDeviceInfo(command); + } else if (command.type == RemoteCommandType.syncState) { + _handleSyncState(command); } else if (command.type == RemoteCommandType.ping || command.type == RemoteCommandType.pong || command.type == RemoteCommandType.ack) { @@ -185,6 +189,14 @@ class CompanionRemoteProvider with ChangeNotifier { } } + void _handleSyncState(RemoteCommand command) { + final playerActive = command.data?['playerActive'] as bool? ?? false; + if (_isPlayerActive != playerActive) { + _isPlayerActive = playerActive; + notifyListeners(); + } + } + void _cleanupSubscriptions() { _commandSubscription?.cancel(); _commandSubscription = null; @@ -363,6 +375,7 @@ class CompanionRemoteProvider with ChangeNotifier { _cleanupSubscriptions(); _session = null; + _isPlayerActive = false; _intentionalDisconnect = false; notifyListeners(); } diff --git a/lib/screens/companion_remote/mobile_remote_screen.dart b/lib/screens/companion_remote/mobile_remote_screen.dart index 4d39db5d..1dd780a2 100644 --- a/lib/screens/companion_remote/mobile_remote_screen.dart +++ b/lib/screens/companion_remote/mobile_remote_screen.dart @@ -211,10 +211,11 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { child: Column( children: [ SegmentedButton( + showSelectedIcon: false, segments: const [ - ButtonSegment(value: 0, label: Text('Navigate'), icon: Icon(Icons.navigation)), - ButtonSegment(value: 1, label: Text('Playback'), icon: Icon(Icons.play_arrow)), - ButtonSegment(value: 2, label: Text('Quick'), icon: Icon(Icons.flash_on)), + ButtonSegment(value: 0, label: Text('Remote'), icon: Icon(Icons.navigation)), + ButtonSegment(value: 1, label: Text('Play'), icon: Icon(Icons.play_arrow)), + ButtonSegment(value: 2, label: Text('More'), icon: Icon(Icons.flash_on)), ], selected: {_selectedTab}, onSelectionChanged: (Set selection) { @@ -236,6 +237,8 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { } Widget _buildNavigationTab() { + final isPlayerActive = context.watch().isPlayerActive; + return Column( children: [ const SizedBox(height: 16), @@ -253,41 +256,43 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { ), const SizedBox(height: 32), Center(child: _DPad(onCommand: _sendCommand)), - const SizedBox(height: 32), - Text('Tab Navigation', style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.center, - children: [ - _RemoteChip( - icon: Icons.explore, - label: 'Discover', - onPressed: () => _sendCommand(RemoteCommandType.tabDiscover), - ), - _RemoteChip( - icon: Icons.video_library, - label: 'Libraries', - onPressed: () => _sendCommand(RemoteCommandType.tabLibraries), - ), - _RemoteChip( - icon: Icons.search, - label: 'Search', - onPressed: () => _showSearchSheet(switchToSearchTab: true), - ), - _RemoteChip( - icon: Icons.download, - label: 'Downloads', - onPressed: () => _sendCommand(RemoteCommandType.tabDownloads), - ), - _RemoteChip( - icon: Icons.settings, - label: 'Settings', - onPressed: () => _sendCommand(RemoteCommandType.tabSettings), - ), - ], - ), + if (!isPlayerActive) ...[ + const SizedBox(height: 32), + Text('Tab Navigation', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.center, + children: [ + _RemoteChip( + icon: Icons.explore, + label: 'Discover', + onPressed: () => _sendCommand(RemoteCommandType.tabDiscover), + ), + _RemoteChip( + icon: Icons.video_library, + label: 'Libraries', + onPressed: () => _sendCommand(RemoteCommandType.tabLibraries), + ), + _RemoteChip( + icon: Icons.search, + label: 'Search', + onPressed: () => _showSearchSheet(switchToSearchTab: true), + ), + _RemoteChip( + icon: Icons.download, + label: 'Downloads', + onPressed: () => _sendCommand(RemoteCommandType.tabDownloads), + ), + _RemoteChip( + icon: Icons.settings, + label: 'Settings', + onPressed: () => _sendCommand(RemoteCommandType.tabSettings), + ), + ], + ), + ], ], ); } @@ -369,6 +374,8 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { } Widget _buildQuickActionsTab() { + final isPlayerActive = context.watch().isPlayerActive; + return Column( children: [ const SizedBox(height: 16), @@ -377,12 +384,25 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { runSpacing: 12, alignment: WrapAlignment.center, children: [ - _RemoteCard(icon: Icons.search, label: 'Search', onPressed: _showSearchSheet), - _RemoteCard( - icon: Icons.fullscreen, - label: 'Fullscreen', - onPressed: () => _sendCommand(RemoteCommandType.fullscreen), - ), + if (!isPlayerActive) + _RemoteCard(icon: Icons.search, label: 'Search', onPressed: _showSearchSheet), + if (isPlayerActive) ...[ + _RemoteCard( + icon: Icons.fullscreen, + label: 'Fullscreen', + onPressed: () => _sendCommand(RemoteCommandType.fullscreen), + ), + _RemoteCard( + icon: Icons.subtitles, + label: 'Subtitles', + onPressed: () => _sendCommand(RemoteCommandType.subtitles), + ), + _RemoteCard( + icon: Icons.audiotrack, + label: 'Audio', + onPressed: () => _sendCommand(RemoteCommandType.audioTracks), + ), + ], ], ), ], @@ -397,16 +417,17 @@ class _DPad extends StatelessWidget { @override Widget build(BuildContext context) { - const size = 80.0; - const centerSize = 60.0; + const size = 72.0; + const gap = 8.0; + const total = size * 3 + gap * 2; return SizedBox( - width: size * 3, - height: size * 3, + width: total, + height: total, child: Stack( children: [ Positioned( - left: size, + left: size + gap, top: 0, child: _DPadButton( icon: Icons.arrow_drop_up, @@ -415,7 +436,7 @@ class _DPad extends StatelessWidget { ), ), Positioned( - left: size, + left: size + gap, bottom: 0, child: _DPadButton( icon: Icons.arrow_drop_down, @@ -425,7 +446,7 @@ class _DPad extends StatelessWidget { ), Positioned( left: 0, - top: size, + top: size + gap, child: _DPadButton( icon: Icons.arrow_left, onPressed: () => onCommand(RemoteCommandType.dpadLeft), @@ -434,7 +455,7 @@ class _DPad extends StatelessWidget { ), Positioned( right: 0, - top: size, + top: size + gap, child: _DPadButton( icon: Icons.arrow_right, onPressed: () => onCommand(RemoteCommandType.dpadRight), @@ -442,13 +463,13 @@ class _DPad extends StatelessWidget { ), ), Positioned( - left: (size * 3 - centerSize) / 2, - top: (size * 3 - centerSize) / 2, + left: size + gap, + top: size + gap, child: _DPadButton( icon: Icons.check, label: 'OK', onPressed: () => onCommand(RemoteCommandType.select), - size: centerSize, + size: size, isPrimary: true, ), ), diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 52454e3e..9aef9df5 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -22,7 +22,10 @@ import '../models/plex_media_info.dart'; import '../providers/download_provider.dart'; import '../providers/multi_server_provider.dart'; import '../providers/playback_state_provider.dart'; +import '../models/companion_remote/remote_command_type.dart'; +import '../providers/companion_remote_provider.dart'; import '../services/companion_remote/companion_remote_receiver.dart'; +import '../services/macos_window_service.dart'; import '../services/discord_rpc_service.dart'; import '../services/episode_navigation_service.dart'; import '../services/media_controls_manager.dart'; @@ -45,6 +48,7 @@ import '../utils/platform_detector.dart'; import '../utils/provider_extensions.dart'; import '../utils/language_codes.dart'; import '../utils/snackbar_helper.dart'; +import '../utils/track_label_builder.dart' as tlb; import '../utils/plex_url_helper.dart'; import '../utils/video_player_navigation.dart'; import '../widgets/video_controls/video_controls.dart'; @@ -134,6 +138,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Watch Together provider reference (stored early to use in dispose) WatchTogetherProvider? _watchTogetherProvider; + // Companion remote state (stored early for use in dispose) + CompanionRemoteProvider? _companionRemoteProvider; + VoidCallback? _savedOnHome; + /// Get the correct PlexClient for this metadata's server PlexClient _getClientForMetadata(BuildContext context) { return context.getClientForServer(widget.metadata.serverId!); @@ -1174,6 +1182,24 @@ class VideoPlayerScreenState extends State with WidgetsBindin player!.setVolume(newVolume); settings.setVolume(newVolume); }; + receiver.onSubtitles = _cycleSubtitleTrack; + receiver.onAudioTracks = _cycleAudioTrack; + receiver.onFullscreen = _toggleFullscreen; + + // Override home to exit the player first (main screen handler runs after pop) + _savedOnHome = receiver.onHome; + receiver.onHome = () { + if (mounted) _handleBackButton(); + }; + + // Store provider reference for use in dispose and notify remote + try { + _companionRemoteProvider = context.read(); + _companionRemoteProvider!.sendCommand( + RemoteCommandType.syncState, + data: {'playerActive': true}, + ); + } catch (_) {} } void _cleanupCompanionRemoteCallbacks() { @@ -1186,6 +1212,84 @@ class VideoPlayerScreenState extends State with WidgetsBindin receiver.onVolumeUp = null; receiver.onVolumeDown = null; receiver.onVolumeMute = null; + receiver.onSubtitles = null; + receiver.onAudioTracks = null; + receiver.onFullscreen = null; + receiver.onHome = _savedOnHome; + _savedOnHome = null; + + // Notify remote that player is no longer active + _companionRemoteProvider?.sendCommand( + RemoteCommandType.syncState, + data: {'playerActive': false}, + ); + _companionRemoteProvider = null; + } + + void _cycleSubtitleTrack() { + if (player == null) return; + final tracks = player!.state.tracks.subtitle.where((t) => t.id != 'auto').toList(); + if (tracks.isEmpty) return; + + final current = player!.state.track.subtitle; + // tracks includes 'no' (off). Find current index and advance. + final currentIndex = tracks.indexWhere((t) => t.id == current?.id); + final nextIndex = (currentIndex + 1) % tracks.length; + final next = tracks[nextIndex]; + player!.selectSubtitleTrack(next); + _onSubtitleTrackChanged(next); + + if (mounted) { + final label = next.id == 'no' + ? 'Subtitles: Off' + : 'Subtitles: ${tlb.TrackLabelBuilder.buildSubtitleLabel(title: next.title, language: next.language, codec: next.codec, index: nextIndex)}'; + showAppSnackBar(context, label, duration: const Duration(seconds: 1)); + } + } + + void _cycleAudioTrack() { + if (player == null) return; + final tracks = player!.state.tracks.audio.where((t) => t.id != 'auto' && t.id != 'no').toList(); + if (tracks.length <= 1) return; + + final current = player!.state.track.audio; + final currentIndex = tracks.indexWhere((t) => t.id == current?.id); + final nextIndex = (currentIndex + 1) % tracks.length; + final next = tracks[nextIndex]; + player!.selectAudioTrack(next); + _onAudioTrackChanged(next); + + if (mounted) { + final label = 'Audio: ${tlb.TrackLabelBuilder.buildAudioLabel(title: next.title, language: next.language, codec: next.codec, channelsCount: next.channelsCount, index: nextIndex)}'; + showAppSnackBar(context, label, duration: const Duration(seconds: 1)); + } + } + + Future _toggleFullscreen() async { + if (PlatformDetector.isMobile(context)) return; + final isCurrentlyFullscreen = await windowManager.isFullScreen(); + if (Platform.isMacOS) { + if (isCurrentlyFullscreen) { + await MacOSWindowService.exitFullscreen(); + } else { + await MacOSWindowService.enterFullscreen(); + } + } else { + await windowManager.setFullScreen(!isCurrentlyFullscreen); + } + } + + /// Exit fullscreen before leaving the player (Windows/Linux only). + /// macOS is excluded because we can't distinguish native fullscreen + /// from maximized state, so we leave the window state unchanged. + Future _exitFullscreenIfNeeded() async { + if (Platform.isWindows || Platform.isLinux) { + final isFullscreen = await windowManager.isFullScreen(); + if (isFullscreen) { + await windowManager.setFullScreen(false); + await Future.delayed(const Duration(milliseconds: 100)); + } + } } /// Handle back button press @@ -1207,16 +1311,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (confirmed && mounted) { await _watchTogetherProvider!.leaveSession(); if (mounted) { - // Exit fullscreen before leaving player (Windows/Linux only) - if (Platform.isWindows || Platform.isLinux) { - final isFullscreen = await windowManager.isFullScreen(); - if (isFullscreen) { - await windowManager.setFullScreen(false); - // Wait for a frame to allow window manager to process the fullscreen exit - await Future.delayed(const Duration(milliseconds: 100)); - if (!mounted) return; - } - } + await _exitFullscreenIfNeeded(); if (!mounted) return; _isExiting.value = true; Navigator.of(context).pop(true); @@ -1225,16 +1320,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin return; } - // Exit fullscreen before leaving player (Windows/Linux only) - if (Platform.isWindows || Platform.isLinux) { - final isFullscreen = await windowManager.isFullScreen(); - if (isFullscreen) { - await windowManager.setFullScreen(false); - // Wait for a frame to allow window manager to process the fullscreen exit - await Future.delayed(const Duration(milliseconds: 100)); - if (!mounted) return; - } - } + await _exitFullscreenIfNeeded(); // Default behavior for hosts or non-session users if (!mounted) return; diff --git a/lib/services/companion_remote/companion_remote_receiver.dart b/lib/services/companion_remote/companion_remote_receiver.dart index e9d16c78..77a38097 100644 --- a/lib/services/companion_remote/companion_remote_receiver.dart +++ b/lib/services/companion_remote/companion_remote_receiver.dart @@ -37,6 +37,9 @@ class CompanionRemoteReceiver { VoidCallback? onVolumeUp; VoidCallback? onVolumeDown; VoidCallback? onVolumeMute; + VoidCallback? onSubtitles; + VoidCallback? onAudioTracks; + VoidCallback? onFullscreen; void handleCommand(RemoteCommand command, BuildContext? context) { appLogger.d('CompanionRemoteReceiver: Handling command: ${command.type}'); @@ -109,17 +112,23 @@ class CompanionRemoteReceiver { onPreviousTrack?.call(); case RemoteCommandType.subtitles: + onSubtitles?.call(); case RemoteCommandType.audioTracks: - break; // No-op: track cycling not yet implemented + onAudioTracks?.call(); case RemoteCommandType.fullscreen: - simulateKeyPress(LogicalKeyboardKey.keyF); + if (onFullscreen != null) { + onFullscreen!.call(); + } else { + simulateKeyPress(LogicalKeyboardKey.keyF); + } case RemoteCommandType.ping: case RemoteCommandType.pong: case RemoteCommandType.ack: case RemoteCommandType.deviceInfo: case RemoteCommandType.disconnect: + case RemoteCommandType.syncState: break; default: diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index 81ff4678..ab6b483f 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -258,6 +258,7 @@ class _PlexVideoControlsState extends State with WindowListen _initAlwaysOnTopState(); } + // Focus play/pause button on first frame if in keyboard mode WidgetsBinding.instance.addPostFrameCallback((_) { _focusPlayPauseIfKeyboardMode(); @@ -733,7 +734,12 @@ class _PlexVideoControlsState extends State with WindowListen } void _updateTrafficLightVisibility() async { - await MacOSWindowService.setTrafficLightsVisible(_showControls); + // When maximized or fullscreen, always keep traffic lights visible so the + // user can reach them without the controls-hide-on-mouse-leave race. + // In normal windowed mode, toggle with controls as before. + final isMaximizedOrFullscreen = await windowManager.isMaximized() || await windowManager.isFullScreen(); + final visible = isMaximizedOrFullscreen ? true : _showControls; + await MacOSWindowService.setTrafficLightsVisible(visible); } /// Check whether PiP is supported on this device From 451cf8ae594f067b45a3e559d6411af7526a7b7d Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:47:06 +0100 Subject: [PATCH 8/8] fix: auto-hide cursor in keyboard mode --- lib/focus/input_mode_tracker.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/focus/input_mode_tracker.dart b/lib/focus/input_mode_tracker.dart index aeef05cd..0d317a3a 100644 --- a/lib/focus/input_mode_tracker.dart +++ b/lib/focus/input_mode_tracker.dart @@ -117,7 +117,15 @@ class _InputModeTrackerState extends State { onPointerDown: (_) => _setMode(InputMode.pointer), onPointerHover: (_) => _setMode(InputMode.pointer), behavior: HitTestBehavior.translucent, - child: _InputModeProvider(mode: _mode, child: widget.child), + child: MouseRegion( + cursor: _mode == InputMode.keyboard + ? SystemMouseCursors.none + : MouseCursor.defer, + child: IgnorePointer( + ignoring: _mode == InputMode.keyboard, + child: _InputModeProvider(mode: _mode, child: widget.child), + ), + ), ); } }