feat: replace QR remote pairing with encrypted LAN discovery

Replace session ID + PIN auth with HKDF-derived home secret and
per-session AES-256-GCM encrypted channel. Auto-start remote server
on host devices, auto-discover hosts via authenticated UDP broadcast.
This commit is contained in:
edde746
2026-04-14 12:33:40 +02:00
parent a55650bc40
commit d093023ce7
44 changed files with 2799 additions and 2390 deletions
+26 -36
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Tillad mobilenheder på dit netværk at styre denne app"
}, },
"search": { "search": {
"hint": "Søg film, serier, musik...", "hint": "Søg film, serier, musik...",
@@ -778,48 +780,36 @@
"controlDesktop": "Styr en desktopenhed", "controlDesktop": "Styr en desktopenhed",
"connectedTo": "Forbundet til ${name}", "connectedTo": "Forbundet til ${name}",
"session": { "session": {
"creatingSession": "Opretter fjernsession...", "startingServer": "Starter fjernserver...",
"failedToCreate": "Kunne ikke oprette fjernsession:", "failedToCreate": "Kunne ikke starte fjernserver:",
"noSession": "Ingen session tilgængelig", "hostAddress": "Værtsadresse",
"scanQrCode": "Scan QR-kode",
"orEnterManually": "Eller indtast manuelt",
"hostAddress": "Værtadresse",
"sessionId": "Sessions-ID",
"pin": "PIN",
"connected": "Forbundet", "connected": "Forbundet",
"waitingForConnection": "Venter på forbindelse...", "serverRunning": "Fjernserver aktiv",
"serverStopped": "Fjernserver stoppet",
"serverRunningDescription": "Mobilenheder på dit netværk kan finde og oprette forbindelse til denne app",
"serverStoppedDescription": "Start serveren for at tillade mobilenheder at oprette forbindelse",
"usePhoneToControl": "Brug din mobilenhed til at styre denne app", "usePhoneToControl": "Brug din mobilenhed til at styre denne app",
"copiedToClipboard": "${label} kopieret til udklipsholder", "startServer": "Start server",
"copyToClipboard": "Kopiér til udklipsholder", "stopServer": "Stop server",
"newSession": "Ny session",
"minimize": "Minimér" "minimize": "Minimér"
}, },
"pairing": { "pairing": {
"scan": "Scan", "pairWithDesktop": "Opret forbindelse til computer",
"manual": "Manuel", "discoveryDescription": "Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk",
"pairWithDesktop": "Par med desktop",
"enterSessionDetails": "Indtast sessionsdetaljerne vist på din desktopenhed",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Indtast 8-tegns sessions-ID", "connecting": "Opretter forbindelse...",
"pinHint": "Indtast 6-cifret PIN", "searchingForDevices": "Søger efter enheder...",
"connecting": "Forbinder...", "noDevicesFound": "Ingen enheder fundet på dit netværk",
"tips": "Tips", "noDevicesHint": "Sørg for, at Plezy er åben på din computer, og at begge enheder er på det samme WiFi-netværk",
"tipDesktop": "Åbn Plezy på din desktop og aktivér fjernbetjening fra indstillinger eller menu", "availableDevices": "Tilgængelige enheder",
"tipScan": "Brug fanen Scan til hurtigt at parre ved at scanne QR-koden på din desktop", "manualConnection": "Manuel forbindelse",
"tipWifi": "Sørg for, at begge enheder er på det samme WiFi-netværk", "cryptoInitFailed": "Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.",
"cameraPermissionRequired": "Kameratilladelse er påkrævet for at scanne QR-koder.\nGiv kameraadgang i enhedsindstillinger.", "validationHostRequired": "Angiv venligst værtsadresse",
"cameraError": "Kunne ikke starte kamera: ${error}",
"scanInstruction": "Ret kameraet mod QR-koden vist på din desktop",
"invalidQrCode": "Ugyldigt QR-kodeformat",
"validationHostRequired": "Indtast værtadresse",
"validationHostFormat": "Format skal være IP:port (f.eks. 192.168.1.100:48632)", "validationHostFormat": "Format skal være IP:port (f.eks. 192.168.1.100:48632)",
"validationSessionIdRequired": "Indtast et sessions-ID", "connectionTimedOut": "Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.",
"validationSessionIdLength": "Sessions-ID skal være 8 tegn", "sessionNotFound": "Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.",
"validationPinRequired": "Indtast en PIN", "authFailed": "Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.",
"validationPinLength": "PIN skal være 6 cifre", "failedToConnect": "Kunne ikke oprette forbindelse: ${error}"
"connectionTimedOut": "Forbindelsen fik timeout. Tjek sessions-ID og PIN.",
"sessionNotFound": "Kunne ikke finde sessionen. Tjek dine oplysninger.",
"failedToConnect": "Kunne ikke forbinde: ${error}"
}, },
"remote": { "remote": {
"disconnectConfirm": "Vil du afbryde fra fjernsessionen?", "disconnectConfirm": "Vil du afbryde fra fjernsessionen?",
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Mobilgeräten in deinem Netzwerk erlauben, diese App zu steuern"
}, },
"search": { "search": {
"hint": "Filme, Serien, Musik suchen...", "hint": "Filme, Serien, Musik suchen...",
@@ -778,47 +780,35 @@
"controlDesktop": "Ein Desktop-Gerät steuern", "controlDesktop": "Ein Desktop-Gerät steuern",
"connectedTo": "Verbunden mit ${name}", "connectedTo": "Verbunden mit ${name}",
"session": { "session": {
"creatingSession": "Fernsteuerungssitzung wird erstellt...", "startingServer": "Remote-Server wird gestartet...",
"failedToCreate": "Fernsteuerungssitzung konnte nicht erstellt werden:", "failedToCreate": "Remote-Server konnte nicht gestartet werden:",
"noSession": "Keine Sitzung verfügbar",
"scanQrCode": "QR-Code scannen",
"orEnterManually": "Oder manuell eingeben",
"hostAddress": "Host-Adresse", "hostAddress": "Host-Adresse",
"sessionId": "Sitzungs-ID",
"pin": "PIN",
"connected": "Verbunden", "connected": "Verbunden",
"waitingForConnection": "Warte auf Verbindung...", "serverRunning": "Remote-Server aktiv",
"serverStopped": "Remote-Server gestoppt",
"serverRunningDescription": "Mobilgeräte in deinem Netzwerk können diese App finden und sich verbinden",
"serverStoppedDescription": "Starte den Server, um Mobilgeräten die Verbindung zu ermöglichen",
"usePhoneToControl": "Verwende dein Mobilgerät, um diese App zu steuern", "usePhoneToControl": "Verwende dein Mobilgerät, um diese App zu steuern",
"copiedToClipboard": "${label} in Zwischenablage kopiert", "startServer": "Server starten",
"copyToClipboard": "In Zwischenablage kopieren", "stopServer": "Server stoppen",
"newSession": "Neue Sitzung",
"minimize": "Minimieren" "minimize": "Minimieren"
}, },
"pairing": { "pairing": {
"scan": "Scannen", "pairWithDesktop": "Mit Desktop verbinden",
"manual": "Manuell", "discoveryDescription": "Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch",
"pairWithDesktop": "Mit Desktop koppeln",
"enterSessionDetails": "Gib die Sitzungsdetails ein, die auf deinem Desktop-Gerät angezeigt werden",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8-stellige Sitzungs-ID eingeben", "connecting": "Verbinden...",
"pinHint": "6-stellige PIN eingeben", "searchingForDevices": "Suche nach Geräten...",
"connecting": "Verbinde...", "noDevicesFound": "Keine Geräte in deinem Netzwerk gefunden",
"tips": "Tipps", "noDevicesHint": "Stelle sicher, dass Plezy auf deinem Desktop geöffnet ist und beide Geräte im selben WLAN sind",
"tipDesktop": "Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü", "availableDevices": "Verfügbare Geräte",
"tipScan": "Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln", "manualConnection": "Manuelle Verbindung",
"tipWifi": "Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind", "cryptoInitFailed": "Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.",
"cameraPermissionRequired": "Kameraberechtigung wird zum Scannen von QR-Codes benötigt.\nBitte erteile den Kamerazugriff in den Geräteeinstellungen.",
"cameraError": "Kamera konnte nicht gestartet werden: ${error}",
"scanInstruction": "Richte deine Kamera auf den QR-Code auf deinem Desktop",
"invalidQrCode": "Ungültiges QR-Code-Format",
"validationHostRequired": "Bitte Host-Adresse eingeben", "validationHostRequired": "Bitte Host-Adresse eingeben",
"validationHostFormat": "Format muss IP:Port sein (z.B. 192.168.1.100:48632)", "validationHostFormat": "Format muss IP:Port sein (z.B. 192.168.1.100:48632)",
"validationSessionIdRequired": "Bitte Sitzungs-ID eingeben", "connectionTimedOut": "Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.",
"validationSessionIdLength": "Sitzungs-ID muss 8 Zeichen lang sein", "sessionNotFound": "Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.",
"validationPinRequired": "Bitte PIN eingeben", "authFailed": "Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.",
"validationPinLength": "PIN muss 6 Ziffern haben",
"connectionTimedOut": "Zeitüberschreitung. Bitte Sitzungs-ID und PIN überprüfen.",
"sessionNotFound": "Sitzung nicht gefunden. Bitte Zugangsdaten überprüfen.",
"failedToConnect": "Verbindung fehlgeschlagen: ${error}" "failedToConnect": "Verbindung fehlgeschlagen: ${error}"
}, },
"remote": { "remote": {
+21 -31
View File
@@ -209,6 +209,8 @@
"maxVolumePercent": "${percent}%", "maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence", "discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Show what you're watching on Discord", "discordRichPresenceDescription": "Show what you're watching on Discord",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Allow mobile devices on your network to control this app",
"autoPip": "Auto Picture-in-Picture", "autoPip": "Auto Picture-in-Picture",
"autoPipDescription": "Automatically enter picture-in-picture when leaving the app during playback", "autoPipDescription": "Automatically enter picture-in-picture when leaving the app during playback",
"matchContentFrameRate": "Match Content Frame Rate", "matchContentFrameRate": "Match Content Frame Rate",
@@ -778,47 +780,35 @@
"controlDesktop": "Control a desktop device", "controlDesktop": "Control a desktop device",
"connectedTo": "Connected to ${name}", "connectedTo": "Connected to ${name}",
"session": { "session": {
"creatingSession": "Creating remote session...", "startingServer": "Starting remote server...",
"failedToCreate": "Failed to create remote session:", "failedToCreate": "Failed to start remote server:",
"noSession": "No session available",
"scanQrCode": "Scan QR Code",
"orEnterManually": "Or enter manually",
"hostAddress": "Host Address", "hostAddress": "Host Address",
"sessionId": "Session ID",
"pin": "PIN",
"connected": "Connected", "connected": "Connected",
"waitingForConnection": "Waiting for connection...", "serverRunning": "Remote server active",
"serverStopped": "Remote server stopped",
"serverRunningDescription": "Mobile devices on your network can discover and connect to this app",
"serverStoppedDescription": "Start the server to allow mobile devices to connect",
"usePhoneToControl": "Use your mobile device to control this app", "usePhoneToControl": "Use your mobile device to control this app",
"copiedToClipboard": "${label} copied to clipboard", "startServer": "Start Server",
"copyToClipboard": "Copy to clipboard", "stopServer": "Stop Server",
"newSession": "New Session",
"minimize": "Minimize" "minimize": "Minimize"
}, },
"pairing": { "pairing": {
"scan": "Scan", "pairWithDesktop": "Connect to Desktop",
"manual": "Manual", "discoveryDescription": "Devices on your network running Plezy with the same Plex account will appear automatically",
"pairWithDesktop": "Pair with Desktop",
"enterSessionDetails": "Enter the session details shown on your desktop device",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Enter 8-character session ID",
"pinHint": "Enter 6-digit PIN",
"connecting": "Connecting...", "connecting": "Connecting...",
"tips": "Tips", "searchingForDevices": "Looking for devices...",
"tipDesktop": "Open Plezy on your desktop and enable Companion Remote from settings or menu", "noDevicesFound": "No devices found on your network",
"tipScan": "Use the Scan tab to quickly pair by scanning the QR code on your desktop", "noDevicesHint": "Make sure Plezy is open on your desktop and both devices are on the same WiFi network",
"tipWifi": "Make sure both devices are on the same WiFi network", "availableDevices": "Available Devices",
"cameraPermissionRequired": "Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.", "manualConnection": "Manual Connection",
"cameraError": "Could not start camera: ${error}", "cryptoInitFailed": "Could not initialize secure connection. Make sure you are signed in to a Plex account.",
"scanInstruction": "Point your camera at the QR code shown on your desktop",
"invalidQrCode": "Invalid QR code format",
"validationHostRequired": "Please enter host address", "validationHostRequired": "Please enter host address",
"validationHostFormat": "Format must be IP:port (e.g., 192.168.1.100:48632)", "validationHostFormat": "Format must be IP:port (e.g., 192.168.1.100:48632)",
"validationSessionIdRequired": "Please enter a session ID", "connectionTimedOut": "Connection timed out. Make sure both devices are on the same network.",
"validationSessionIdLength": "Session ID must be 8 characters", "sessionNotFound": "Could not find the device. Make sure Plezy is running on the host.",
"validationPinRequired": "Please enter a PIN", "authFailed": "Authentication failed. Make sure both devices are on the same Plex account.",
"validationPinLength": "PIN must be 6 digits",
"connectionTimedOut": "Connection timed out. Please check the session ID and PIN.",
"sessionNotFound": "Could not find the session. Please check your credentials.",
"failedToConnect": "Failed to connect: ${error}" "failedToConnect": "Failed to connect: ${error}"
}, },
"remote": { "remote": {
+25 -35
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Servidor de control remoto",
"companionRemoteServerDescription": "Permitir que dispositivos móviles en tu red controlen esta aplicación"
}, },
"search": { "search": {
"hint": "Buscar películas, series, música...", "hint": "Buscar películas, series, música...",
@@ -778,47 +780,35 @@
"controlDesktop": "Controlar un dispositivo de escritorio", "controlDesktop": "Controlar un dispositivo de escritorio",
"connectedTo": "Conectado a ${name}", "connectedTo": "Conectado a ${name}",
"session": { "session": {
"creatingSession": "Creando sesión remota...", "startingServer": "Iniciando servidor remoto...",
"failedToCreate": "Error al crear la sesión remota:", "failedToCreate": "Error al iniciar el servidor remoto:",
"noSession": "No hay sesión disponible",
"scanQrCode": "Escanear código QR",
"orEnterManually": "O introducir manualmente",
"hostAddress": "Dirección del host", "hostAddress": "Dirección del host",
"sessionId": "ID de sesión",
"pin": "PIN",
"connected": "Conectado", "connected": "Conectado",
"waitingForConnection": "Esperando conexión...", "serverRunning": "Servidor remoto activo",
"usePhoneToControl": "Usa tu dispositivo móvil para controlar esta app", "serverStopped": "Servidor remoto detenido",
"copiedToClipboard": "${label} copiado al portapapeles", "serverRunningDescription": "Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación",
"copyToClipboard": "Copiar al portapapeles", "serverStoppedDescription": "Inicia el servidor para permitir que los dispositivos móviles se conecten",
"newSession": "Nueva sesión", "usePhoneToControl": "Usa tu dispositivo móvil para controlar esta aplicación",
"startServer": "Iniciar servidor",
"stopServer": "Detener servidor",
"minimize": "Minimizar" "minimize": "Minimizar"
}, },
"pairing": { "pairing": {
"scan": "Escanear", "pairWithDesktop": "Conectar al escritorio",
"manual": "Manual", "discoveryDescription": "Los dispositivos en tu red que ejecutan Plezy con la misma cuenta de Plex aparecerán automáticamente",
"pairWithDesktop": "Emparejar con escritorio",
"enterSessionDetails": "Introduce los datos de la sesión que aparecen en tu dispositivo de escritorio",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Introduce ID de sesión de 8 caracteres",
"pinHint": "Introduce PIN de 6 dígitos",
"connecting": "Conectando...", "connecting": "Conectando...",
"tips": "Consejos", "searchingForDevices": "Buscando dispositivos...",
"tipDesktop": "Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú", "noDevicesFound": "No se encontraron dispositivos en tu red",
"tipScan": "Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio", "noDevicesHint": "Asegúrate de que Plezy esté abierto en tu escritorio y que ambos dispositivos estén en la misma red WiFi",
"tipWifi": "Asegúrate de que ambos dispositivos estén en la misma red WiFi", "availableDevices": "Dispositivos disponibles",
"cameraPermissionRequired": "Se necesita permiso de cámara para escanear códigos QR.\nPor favor, concede acceso a la cámara en los ajustes de tu dispositivo.", "manualConnection": "Conexión manual",
"cameraError": "No se pudo iniciar la cámara: ${error}", "cryptoInitFailed": "No se pudo inicializar la conexión segura. Asegúrate de haber iniciado sesión en una cuenta de Plex.",
"scanInstruction": "Apunta tu cámara al código QR que aparece en tu escritorio", "validationHostRequired": "Ingresa la dirección del host",
"invalidQrCode": "Formato de código QR no válido", "validationHostFormat": "El formato debe ser IP:puerto (ej. 192.168.1.100:48632)",
"validationHostRequired": "Por favor, introduce la dirección del host", "connectionTimedOut": "Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.",
"validationHostFormat": "El formato debe ser IP:puerto (ej., 192.168.1.100:48632)", "sessionNotFound": "No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.",
"validationSessionIdRequired": "Por favor, introduce un ID de sesión", "authFailed": "Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.",
"validationSessionIdLength": "El ID de sesión debe tener 8 caracteres",
"validationPinRequired": "Por favor, introduce un PIN",
"validationPinLength": "El PIN debe tener 6 dígitos",
"connectionTimedOut": "Tiempo de conexión agotado. Verifica el ID de sesión y el PIN.",
"sessionNotFound": "No se encontró la sesión. Verifica tus credenciales.",
"failedToConnect": "Error al conectar: ${error}" "failedToConnect": "Error al conectar: ${error}"
}, },
"remote": { "remote": {
+24 -34
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Serveur de télécommande",
"companionRemoteServerDescription": "Autoriser les appareils mobiles de votre réseau à contrôler cette application"
}, },
"search": { "search": {
"hint": "Rechercher des films, des séries, de la musique...", "hint": "Rechercher des films, des séries, de la musique...",
@@ -778,47 +780,35 @@
"controlDesktop": "Contrôler un appareil de bureau", "controlDesktop": "Contrôler un appareil de bureau",
"connectedTo": "Connecté à ${name}", "connectedTo": "Connecté à ${name}",
"session": { "session": {
"creatingSession": "Création de la session distante...", "startingServer": "Démarrage du serveur distant...",
"failedToCreate": "Échec de la création de la session distante :", "failedToCreate": "Échec du démarrage du serveur distant :",
"noSession": "Aucune session disponible",
"scanQrCode": "Scanner le QR Code",
"orEnterManually": "Ou saisir manuellement",
"hostAddress": "Adresse de l'hôte", "hostAddress": "Adresse de l'hôte",
"sessionId": "ID de session",
"pin": "PIN",
"connected": "Connecté", "connected": "Connecté",
"waitingForConnection": "En attente de connexion...", "serverRunning": "Serveur distant actif",
"serverStopped": "Serveur distant arrêté",
"serverRunningDescription": "Les appareils mobiles de votre réseau peuvent découvrir et se connecter à cette application",
"serverStoppedDescription": "Démarrez le serveur pour permettre aux appareils mobiles de se connecter",
"usePhoneToControl": "Utilisez votre appareil mobile pour contrôler cette application", "usePhoneToControl": "Utilisez votre appareil mobile pour contrôler cette application",
"copiedToClipboard": "${label} copié dans le presse-papiers", "startServer": "Démarrer le serveur",
"copyToClipboard": "Copier dans le presse-papiers", "stopServer": "Arrêter le serveur",
"newSession": "Nouvelle session",
"minimize": "Réduire" "minimize": "Réduire"
}, },
"pairing": { "pairing": {
"scan": "Scanner", "pairWithDesktop": "Se connecter au bureau",
"manual": "Manuel", "discoveryDescription": "Les appareils de votre réseau exécutant Plezy avec le même compte Plex apparaîtront automatiquement",
"pairWithDesktop": "Jumeler avec un bureau",
"enterSessionDetails": "Saisissez les détails de la session affichés sur votre appareil de bureau",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Saisissez l'ID de session à 8 caractères",
"pinHint": "Saisissez le PIN à 6 chiffres",
"connecting": "Connexion...", "connecting": "Connexion...",
"tips": "Conseils", "searchingForDevices": "Recherche d'appareils...",
"tipDesktop": "Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu", "noDevicesFound": "Aucun appareil trouvé sur votre réseau",
"tipScan": "Utilisez l'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau", "noDevicesHint": "Assurez-vous que Plezy est ouvert sur votre bureau et que les deux appareils sont sur le même réseau WiFi",
"tipWifi": "Assurez-vous que les deux appareils sont sur le même réseau WiFi", "availableDevices": "Appareils disponibles",
"cameraPermissionRequired": "L'autorisation de la caméra est requise pour scanner les QR codes.\nVeuillez accorder l'accès à la caméra dans les paramètres de votre appareil.", "manualConnection": "Connexion manuelle",
"cameraError": "Impossible de démarrer la caméra : ${error}", "cryptoInitFailed": "Impossible d'initialiser la connexion sécurisée. Assurez-vous d'être connecté à un compte Plex.",
"scanInstruction": "Pointez votre caméra vers le QR code affiché sur votre bureau", "validationHostRequired": "Veuillez entrer l'adresse de l'hôte",
"invalidQrCode": "Format de QR code invalide", "validationHostFormat": "Le format doit être IP:port (ex. 192.168.1.100:48632)",
"validationHostRequired": "Veuillez saisir l'adresse de l'hôte", "connectionTimedOut": "Délai de connexion dépassé. Assurez-vous que les deux appareils sont sur le même réseau.",
"validationHostFormat": "Le format doit être IP:port (ex : 192.168.1.100:48632)", "sessionNotFound": "Appareil introuvable. Assurez-vous que Plezy est en cours d'exécution sur l'hôte.",
"validationSessionIdRequired": "Veuillez saisir un ID de session", "authFailed": "Échec de l'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.",
"validationSessionIdLength": "L'ID de session doit contenir 8 caractères",
"validationPinRequired": "Veuillez saisir un PIN",
"validationPinLength": "Le PIN doit contenir 6 chiffres",
"connectionTimedOut": "Délai de connexion expiré. Veuillez vérifier l'ID de session et le PIN.",
"sessionNotFound": "Session introuvable. Veuillez vérifier vos identifiants.",
"failedToConnect": "Échec de la connexion : ${error}" "failedToConnect": "Échec de la connexion : ${error}"
}, },
"remote": { "remote": {
+22 -32
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Server telecomando",
"companionRemoteServerDescription": "Consenti ai dispositivi mobili nella tua rete di controllare questa app"
}, },
"search": { "search": {
"hint": "Cerca film. spettacoli, musica...", "hint": "Cerca film. spettacoli, musica...",
@@ -778,47 +780,35 @@
"controlDesktop": "Controlla un dispositivo desktop", "controlDesktop": "Controlla un dispositivo desktop",
"connectedTo": "Connesso a ${name}", "connectedTo": "Connesso a ${name}",
"session": { "session": {
"creatingSession": "Creazione sessione remota...", "startingServer": "Avvio del server remoto...",
"failedToCreate": "Impossibile creare la sessione remota:", "failedToCreate": "Impossibile avviare il server remoto:",
"noSession": "Nessuna sessione disponibile",
"scanQrCode": "Scansiona QR Code",
"orEnterManually": "Oppure inserisci manualmente",
"hostAddress": "Indirizzo host", "hostAddress": "Indirizzo host",
"sessionId": "ID sessione",
"pin": "PIN",
"connected": "Connesso", "connected": "Connesso",
"waitingForConnection": "In attesa di connessione...", "serverRunning": "Server remoto attivo",
"serverStopped": "Server remoto arrestato",
"serverRunningDescription": "I dispositivi mobili nella tua rete possono scoprire e connettersi a questa app",
"serverStoppedDescription": "Avvia il server per consentire ai dispositivi mobili di connettersi",
"usePhoneToControl": "Usa il tuo dispositivo mobile per controllare questa app", "usePhoneToControl": "Usa il tuo dispositivo mobile per controllare questa app",
"copiedToClipboard": "${label} copiato negli appunti", "startServer": "Avvia server",
"copyToClipboard": "Copia negli appunti", "stopServer": "Arresta server",
"newSession": "Nuova sessione",
"minimize": "Riduci" "minimize": "Riduci"
}, },
"pairing": { "pairing": {
"scan": "Scansiona", "pairWithDesktop": "Connetti al desktop",
"manual": "Manuale", "discoveryDescription": "I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente",
"pairWithDesktop": "Associa con desktop",
"enterSessionDetails": "Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Inserisci ID sessione di 8 caratteri",
"pinHint": "Inserisci PIN di 6 cifre",
"connecting": "Connessione...", "connecting": "Connessione...",
"tips": "Suggerimenti", "searchingForDevices": "Ricerca dispositivi...",
"tipDesktop": "Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu", "noDevicesFound": "Nessun dispositivo trovato nella tua rete",
"tipScan": "Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop", "noDevicesHint": "Assicurati che Plezy sia aperto sul tuo desktop e che entrambi i dispositivi siano sulla stessa rete WiFi",
"tipWifi": "Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi", "availableDevices": "Dispositivi disponibili",
"cameraPermissionRequired": "L'autorizzazione della fotocamera è necessaria per scansionare i QR code.\nConcedi l'accesso alla fotocamera nelle impostazioni del dispositivo.", "manualConnection": "Connessione manuale",
"cameraError": "Impossibile avviare la fotocamera: ${error}", "cryptoInitFailed": "Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l'accesso a un account Plex.",
"scanInstruction": "Punta la fotocamera verso il QR code mostrato sul tuo desktop",
"invalidQrCode": "Formato QR code non valido",
"validationHostRequired": "Inserisci l'indirizzo host", "validationHostRequired": "Inserisci l'indirizzo host",
"validationHostFormat": "Il formato deve essere IP:porta (es. 192.168.1.100:48632)", "validationHostFormat": "Il formato deve essere IP:porta (es. 192.168.1.100:48632)",
"validationSessionIdRequired": "Inserisci un ID sessione", "connectionTimedOut": "Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.",
"validationSessionIdLength": "L'ID sessione deve essere di 8 caratteri", "sessionNotFound": "Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull'host.",
"validationPinRequired": "Inserisci un PIN", "authFailed": "Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.",
"validationPinLength": "Il PIN deve essere di 6 cifre",
"connectionTimedOut": "Connessione scaduta. Verifica l'ID sessione e il PIN.",
"sessionNotFound": "Sessione non trovata. Verifica le tue credenziali.",
"failedToConnect": "Connessione fallita: ${error}" "failedToConnect": "Connessione fallita: ${error}"
}, },
"remote": { "remote": {
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "コンパニオンリモートサーバー",
"companionRemoteServerDescription": "ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする"
}, },
"search": { "search": {
"hint": "映画、番組、音楽を検索...", "hint": "映画、番組、音楽を検索...",
@@ -778,47 +780,35 @@
"controlDesktop": "デスクトップデバイスを操作", "controlDesktop": "デスクトップデバイスを操作",
"connectedTo": "${name}に接続中", "connectedTo": "${name}に接続中",
"session": { "session": {
"creatingSession": "リモートセッションを作成中...", "startingServer": "リモートサーバーを起動中...",
"failedToCreate": "リモートセッションの作成に失敗しました:", "failedToCreate": "リモートサーバーの起動に失敗しました:",
"noSession": "利用可能なセッションがありません",
"scanQrCode": "QRコードをスキャン",
"orEnterManually": "または手動で入力",
"hostAddress": "ホストアドレス", "hostAddress": "ホストアドレス",
"sessionId": "セッションID",
"pin": "PIN",
"connected": "接続済み", "connected": "接続済み",
"waitingForConnection": "接続を待機中...", "serverRunning": "リモートサーバー稼働中",
"usePhoneToControl": "モバイルデバイスでこのアプリを操作", "serverStopped": "リモートサーバー停止中",
"copiedToClipboard": "${label}をクリップボードにコピーしました", "serverRunningDescription": "ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます",
"copyToClipboard": "クリップボードにコピー", "serverStoppedDescription": "モバイルデバイスの接続を許可するにはサーバーを起動してください",
"newSession": "新しいセッション", "usePhoneToControl": "モバイルデバイスでこのアプリを操作できます",
"startServer": "サーバーを起動",
"stopServer": "サーバーを停止",
"minimize": "最小化" "minimize": "最小化"
}, },
"pairing": { "pairing": {
"scan": "スキャン", "pairWithDesktop": "デスクトップに接続",
"manual": "手動", "discoveryDescription": "同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます",
"pairWithDesktop": "デスクトップとペアリング",
"enterSessionDetails": "デスクトップデバイスに表示されたセッション情報を入力",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8文字のセッションIDを入力",
"pinHint": "6桁のPINを入力",
"connecting": "接続中...", "connecting": "接続中...",
"tips": "ヒント", "searchingForDevices": "デバイスを検索中...",
"tipDesktop": "デスクトップでPlezyを開き、設定またはメニューからコンパニオンリモートを有効にしてください", "noDevicesFound": "ネットワーク上にデバイスが見つかりません",
"tipScan": "スキャンタブを使用して、デスクトップのQRコードをスキャンして素早くペアリング", "noDevicesHint": "デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください",
"tipWifi": "両方のデバイスが同じWiFiネットワークに接続されていることを確認", "availableDevices": "利用可能なデバイス",
"cameraPermissionRequired": "QRコードをスキャンするにはカメラの権限が必要です。\nデバイスの設定でカメラへのアクセスを許可してください。", "manualConnection": "手動接続",
"cameraError": "カメラを起動できませんでした: ${error}", "cryptoInitFailed": "安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。",
"scanInstruction": "デスクトップに表示されたQRコードにカメラを向けてください",
"invalidQrCode": "無効なQRコード形式",
"validationHostRequired": "ホストアドレスを入力してください", "validationHostRequired": "ホストアドレスを入力してください",
"validationHostFormat": "形式はIP:ポートである必要があります(例: 192.168.1.100:48632", "validationHostFormat": "形式はIP:ポートである必要があります(例: 192.168.1.100:48632",
"validationSessionIdRequired": "セッションIDを入力してください", "connectionTimedOut": "接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください",
"validationSessionIdLength": "セッションIDは8文字である必要があります", "sessionNotFound": "デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。",
"validationPinRequired": "PINを入力してください", "authFailed": "認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください",
"validationPinLength": "PINは6桁である必要があります",
"connectionTimedOut": "接続がタイムアウトしました。セッションIDとPINを確認してください。",
"sessionNotFound": "セッションが見つかりませんでした。認証情報を確認してください。",
"failedToConnect": "接続に失敗しました: ${error}" "failedToConnect": "接続に失敗しました: ${error}"
}, },
"remote": { "remote": {
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "컴패니언 리모트 서버",
"companionRemoteServerDescription": "네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용"
}, },
"search": { "search": {
"hint": "영화, 시리즈, 음악 등을 검색하세요...", "hint": "영화, 시리즈, 음악 등을 검색하세요...",
@@ -778,47 +780,35 @@
"controlDesktop": "데스크톱 기기 제어", "controlDesktop": "데스크톱 기기 제어",
"connectedTo": "${name}에 연결됨", "connectedTo": "${name}에 연결됨",
"session": { "session": {
"creatingSession": "원격 세션 생성 중...", "startingServer": "원격 서버 시작 중...",
"failedToCreate": "원격 세션 생성 실패:", "failedToCreate": "원격 서버를 시작하지 못했습니다:",
"noSession": "사용 가능한 세션이 없습니다",
"scanQrCode": "QR 코드 스캔",
"orEnterManually": "또는 수동으로 입력",
"hostAddress": "호스트 주소", "hostAddress": "호스트 주소",
"sessionId": "세션 ID",
"pin": "PIN",
"connected": "연결됨", "connected": "연결됨",
"waitingForConnection": "연결 대기 중...", "serverRunning": "원격 서버 활성",
"serverStopped": "원격 서버 중지됨",
"serverRunningDescription": "네트워크의 모바일 기기가 이 앱을 검색하고 연결할 수 있습니다",
"serverStoppedDescription": "모바일 기기의 연결을 허용하려면 서버를 시작하세요",
"usePhoneToControl": "모바일 기기로 이 앱을 제어하세요", "usePhoneToControl": "모바일 기기로 이 앱을 제어하세요",
"copiedToClipboard": "${label}이(가) 클립보드에 복사되었습니다", "startServer": "서버 시작",
"copyToClipboard": "클립보드에 복사", "stopServer": "서버 중지",
"newSession": "새 세션",
"minimize": "최소화" "minimize": "최소화"
}, },
"pairing": { "pairing": {
"scan": "스캔", "pairWithDesktop": "데스크톱에 연결",
"manual": "수동", "discoveryDescription": "같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다",
"pairWithDesktop": "데스크톱과 페어링",
"enterSessionDetails": "데스크톱 기기에 표시된 세션 정보를 입력하세요",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8자리 세션 ID 입력",
"pinHint": "6자리 PIN 입력",
"connecting": "연결 중...", "connecting": "연결 중...",
"tips": "팁", "searchingForDevices": "기기 검색 중...",
"tipDesktop": "데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요", "noDevicesFound": "네트워크에서 기기를 찾을 수 없습니다",
"tipScan": "스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다", "noDevicesHint": "데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요",
"tipWifi": "두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요", "availableDevices": "사용 가능한 기기",
"cameraPermissionRequired": "QR 코드를 스캔하려면 카메라 권한이 필요합니다.\n기기 설정에서 카메라 접근을 허용해 주세요.", "manualConnection": "수동 연결",
"cameraError": "카메라를 시작할 수 없습니다: ${error}", "cryptoInitFailed": "보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.",
"scanInstruction": "데스크톱에 표시된 QR 코드에 카메라를 향하세요",
"invalidQrCode": "유효하지 않은 QR 코드 형식",
"validationHostRequired": "호스트 주소를 입력하세요", "validationHostRequired": "호스트 주소를 입력하세요",
"validationHostFormat": "IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)", "validationHostFormat": "형식은 IP:포트야 합니다 (예: 192.168.1.100:48632)",
"validationSessionIdRequired": "세션 ID를 입력하세요", "connectionTimedOut": "연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.",
"validationSessionIdLength": "세션 ID는 8자리여야 합니다", "sessionNotFound": "기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.",
"validationPinRequired": "PIN을 입력하세요", "authFailed": "인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.",
"validationPinLength": "PIN은 6자리여야 합니다",
"connectionTimedOut": "연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.",
"sessionNotFound": "세션을 찾을 수 없습니다. 자격 증명을 확인하세요.",
"failedToConnect": "연결 실패: ${error}" "failedToConnect": "연결 실패: ${error}"
}, },
"remote": { "remote": {
+25 -35
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Companion Remote-server",
"companionRemoteServerDescription": "Tillat mobilenheter på nettverket ditt å styre denne appen"
}, },
"search": { "search": {
"hint": "Søk i filmer, serier, musikk...", "hint": "Søk i filmer, serier, musikk...",
@@ -778,47 +780,35 @@
"controlDesktop": "Kontroller en stasjonær enhet", "controlDesktop": "Kontroller en stasjonær enhet",
"connectedTo": "Tilkoblet ${name}", "connectedTo": "Tilkoblet ${name}",
"session": { "session": {
"creatingSession": "Oppretter fjernøkt...", "startingServer": "Starter fjernserver...",
"failedToCreate": "Kunne ikke opprette fjernøkt:", "failedToCreate": "Kunne ikke starte fjernserver:",
"noSession": "Ingen økt tilgjengelig",
"scanQrCode": "Skann QR-kode",
"orEnterManually": "Eller skriv inn manuelt",
"hostAddress": "Vertsadresse", "hostAddress": "Vertsadresse",
"sessionId": "Økt-ID",
"pin": "PIN",
"connected": "Tilkoblet", "connected": "Tilkoblet",
"waitingForConnection": "Venter på tilkobling...", "serverRunning": "Fjernserver aktiv",
"usePhoneToControl": "Bruk mobilenheten din til å kontrollere denne appen", "serverStopped": "Fjernserver stoppet",
"copiedToClipboard": "${label} kopiert til utklippstavle", "serverRunningDescription": "Mobilenheter på nettverket ditt kan oppdage og koble til denne appen",
"copyToClipboard": "Kopier til utklippstavle", "serverStoppedDescription": "Start serveren for å la mobilenheter koble til",
"newSession": "Ny økt", "usePhoneToControl": "Bruk mobilenheten din til å styre denne appen",
"startServer": "Start server",
"stopServer": "Stopp server",
"minimize": "Minimer" "minimize": "Minimer"
}, },
"pairing": { "pairing": {
"scan": "Skann", "pairWithDesktop": "Koble til datamaskin",
"manual": "Manuell", "discoveryDescription": "Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk",
"pairWithDesktop": "Koble til stasjonær",
"enterSessionDetails": "Skriv inn øktdetaljene som vises på den stasjonære enheten din",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Skriv inn 8-tegns økt-ID",
"pinHint": "Skriv inn 6-sifret PIN",
"connecting": "Kobler til...", "connecting": "Kobler til...",
"tips": "Tips", "searchingForDevices": "Søker etter enheter...",
"tipDesktop": "Åpne Plezy på datamaskinen din og aktiver Følgesvenn-fjernkontroll fra innstillinger eller meny", "noDevicesFound": "Ingen enheter funnet på nettverket ditt",
"tipScan": "Bruk Skann-fanen for å raskt koble til ved å skanne QR-koden på datamaskinen din", "noDevicesHint": "Sørg for at Plezy er åpen på datamaskinen din og at begge enhetene er på samme WiFi-nettverk",
"tipWifi": "Sørg for at begge enhetene er på samme WiFi-nettverk", "availableDevices": "Tilgjengelige enheter",
"cameraPermissionRequired": "Kameratillatelse kreves for å skanne QR-koder.\nVennligst gi kameratilgang i enhetsinnstillingene.", "manualConnection": "Manuell tilkobling",
"cameraError": "Kunne ikke starte kamera: ${error}", "cryptoInitFailed": "Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.",
"scanInstruction": "Pek kameraet mot QR-koden som vises på datamaskinen din", "validationHostRequired": "Vennligst oppgi vertsadresse",
"invalidQrCode": "Ugyldig QR-kodeformat", "validationHostFormat": "Format må være IP:port (f.eks. 192.168.1.100:48632)",
"validationHostRequired": "Vennligst skriv inn vertsadresse", "connectionTimedOut": "Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.",
"validationHostFormat": "Formatet må være IP:port (f.eks. 192.168.1.100:48632)", "sessionNotFound": "Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.",
"validationSessionIdRequired": "Vennligst skriv inn en økt-ID", "authFailed": "Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.",
"validationSessionIdLength": "Økt-ID må være 8 tegn",
"validationPinRequired": "Vennligst skriv inn en PIN",
"validationPinLength": "PIN må være 6 sifre",
"connectionTimedOut": "Tilkoblingen ble tidsavbrutt. Sjekk økt-ID og PIN.",
"sessionNotFound": "Kunne ikke finne økten. Sjekk legitimasjonen din.",
"failedToConnect": "Kunne ikke koble til: ${error}" "failedToConnect": "Kunne ikke koble til: ${error}"
}, },
"remote": { "remote": {
+24 -34
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Companion Remote-server",
"companionRemoteServerDescription": "Sta mobiele apparaten op je netwerk toe om deze app te bedienen"
}, },
"search": { "search": {
"hint": "Zoek films, series, muziek...", "hint": "Zoek films, series, muziek...",
@@ -778,48 +780,36 @@
"controlDesktop": "Bedien een desktop-apparaat", "controlDesktop": "Bedien een desktop-apparaat",
"connectedTo": "Verbonden met ${name}", "connectedTo": "Verbonden met ${name}",
"session": { "session": {
"creatingSession": "Externe sessie aanmaken...", "startingServer": "Externe server starten...",
"failedToCreate": "Kan externe sessie niet aanmaken:", "failedToCreate": "Kan externe server niet starten:",
"noSession": "Geen sessie beschikbaar",
"scanQrCode": "Scan QR-code",
"orEnterManually": "Of voer handmatig in",
"hostAddress": "Hostadres", "hostAddress": "Hostadres",
"sessionId": "Sessie-ID",
"pin": "PIN",
"connected": "Verbonden", "connected": "Verbonden",
"waitingForConnection": "Wachten op verbinding...", "serverRunning": "Externe server actief",
"serverStopped": "Externe server gestopt",
"serverRunningDescription": "Mobiele apparaten op je netwerk kunnen deze app ontdekken en ermee verbinden",
"serverStoppedDescription": "Start de server om mobiele apparaten te laten verbinden",
"usePhoneToControl": "Gebruik je mobiele apparaat om deze app te bedienen", "usePhoneToControl": "Gebruik je mobiele apparaat om deze app te bedienen",
"copiedToClipboard": "${label} gekopieerd naar klembord", "startServer": "Server starten",
"copyToClipboard": "Kopieer naar klembord", "stopServer": "Server stoppen",
"newSession": "Nieuwe sessie",
"minimize": "Minimaliseren" "minimize": "Minimaliseren"
}, },
"pairing": { "pairing": {
"scan": "Scannen", "pairWithDesktop": "Verbinden met desktop",
"manual": "Handmatig", "discoveryDescription": "Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch",
"pairWithDesktop": "Koppelen met desktop",
"enterSessionDetails": "Voer de sessiegegevens in die op je desktop-apparaat worden getoond",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Voer 8-tekens sessie-ID in",
"pinHint": "Voer 6-cijferige PIN in",
"connecting": "Verbinden...", "connecting": "Verbinden...",
"tips": "Tips", "searchingForDevices": "Apparaten zoeken...",
"tipDesktop": "Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu", "noDevicesFound": "Geen apparaten gevonden op je netwerk",
"tipScan": "Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen", "noDevicesHint": "Zorg ervoor dat Plezy geopend is op je desktop en dat beide apparaten op hetzelfde WiFi-netwerk zitten",
"tipWifi": "Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten", "availableDevices": "Beschikbare apparaten",
"cameraPermissionRequired": "Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.", "manualConnection": "Handmatige verbinding",
"cameraError": "Kan camera niet starten: ${error}", "cryptoInitFailed": "Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.",
"scanInstruction": "Richt je camera op de QR-code die op je desktop wordt getoond", "validationHostRequired": "Voer het hostadres in",
"invalidQrCode": "Ongeldig QR-codeformaat",
"validationHostRequired": "Voer een hostadres in",
"validationHostFormat": "Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)", "validationHostFormat": "Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)",
"validationSessionIdRequired": "Voer een sessie-ID in", "connectionTimedOut": "Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.",
"validationSessionIdLength": "Sessie-ID moet 8 tekens zijn", "sessionNotFound": "Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.",
"validationPinRequired": "Voer een PIN in", "authFailed": "Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.",
"validationPinLength": "PIN moet 6 cijfers zijn", "failedToConnect": "Kan niet verbinden: ${error}"
"connectionTimedOut": "Verbinding verlopen. Controleer de sessie-ID en PIN.",
"sessionNotFound": "Kan de sessie niet vinden. Controleer je gegevens.",
"failedToConnect": "Verbinden mislukt: ${error}"
}, },
"remote": { "remote": {
"disconnectConfirm": "Wil je de verbinding met de externe sessie verbreken?", "disconnectConfirm": "Wil je de verbinding met de externe sessie verbreken?",
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Serwer zdalnego sterowania",
"companionRemoteServerDescription": "Pozwól urządzeniom mobilnym w sieci sterować tą aplikacją"
}, },
"search": { "search": {
"hint": "Szukaj filmów, seriali, muzyki...", "hint": "Szukaj filmów, seriali, muzyki...",
@@ -778,47 +780,35 @@
"controlDesktop": "Steruj urządzeniem desktop", "controlDesktop": "Steruj urządzeniem desktop",
"connectedTo": "Połączono z ${name}", "connectedTo": "Połączono z ${name}",
"session": { "session": {
"creatingSession": "Tworzenie sesji zdalnej...", "startingServer": "Uruchamianie serwera zdalnego...",
"failedToCreate": "Nie udało się utworzyć sesji zdalnej:", "failedToCreate": "Nie udało się uruchomić serwera zdalnego:",
"noSession": "Brak dostępnej sesji",
"scanQrCode": "Zeskanuj kod QR",
"orEnterManually": "Lub wprowadź ręcznie",
"hostAddress": "Adres hosta", "hostAddress": "Adres hosta",
"sessionId": "ID sesji",
"pin": "PIN",
"connected": "Połączono", "connected": "Połączono",
"waitingForConnection": "Oczekiwanie na połączenie...", "serverRunning": "Serwer zdalny aktywny",
"usePhoneToControl": "Użyj urządzenia mobilnego do sterowania tą aplikacją", "serverStopped": "Serwer zdalny zatrzymany",
"copiedToClipboard": "${label} skopiowane do schowka", "serverRunningDescription": "Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć",
"copyToClipboard": "Kopiuj do schowka", "serverStoppedDescription": "Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych",
"newSession": "Nowa sesja", "usePhoneToControl": "Użyj urządzenia mobilnego, aby sterować tą aplikacją",
"startServer": "Uruchom serwer",
"stopServer": "Zatrzymaj serwer",
"minimize": "Minimalizuj" "minimize": "Minimalizuj"
}, },
"pairing": { "pairing": {
"scan": "Skanuj", "pairWithDesktop": "Połącz z komputerem",
"manual": "Ręcznie", "discoveryDescription": "Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie",
"pairWithDesktop": "Sparuj z komputerem",
"enterSessionDetails": "Wprowadź dane sesji wyświetlone na urządzeniu desktop",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Wprowadź 8-znakowe ID sesji",
"pinHint": "Wprowadź 6-cyfrowy PIN",
"connecting": "Łączenie...", "connecting": "Łączenie...",
"tips": "Wskazówki", "searchingForDevices": "Szukanie urządzeń...",
"tipDesktop": "Otwórz Plezy na komputerze i włącz Pilota zdalnego w ustawieniach lub menu", "noDevicesFound": "Nie znaleziono urządzeń w sieci",
"tipScan": "Użyj zakładki Skanuj, aby szybko sparować skanując kod QR na komputerze", "noDevicesHint": "Upewnij się, że Plezy jest otwarte na komputerze i oba urządzenia są w tej samej sieci WiFi",
"tipWifi": "Upewnij się, że oba urządzenia są w tej samej sieci WiFi", "availableDevices": "Dostępne urządzenia",
"cameraPermissionRequired": "Uprawnienie do kamery jest wymagane do skanowania kodów QR.\nPrzyznaj dostęp do kamery w ustawieniach urządzenia.", "manualConnection": "Połączenie ręczne",
"cameraError": "Nie udało się uruchomić kamery: ${error}", "cryptoInitFailed": "Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.",
"scanInstruction": "Skieruj kamerę na kod QR wyświetlony na komputerze",
"invalidQrCode": "Nieprawidłowy format kodu QR",
"validationHostRequired": "Wprowadź adres hosta", "validationHostRequired": "Wprowadź adres hosta",
"validationHostFormat": "Format musi być IP:port (np. 192.168.1.100:48632)", "validationHostFormat": "Format musi być IP:port (np. 192.168.1.100:48632)",
"validationSessionIdRequired": "Wprowadź ID sesji", "connectionTimedOut": "Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.",
"validationSessionIdLength": "ID sesji musi mieć 8 znaków", "sessionNotFound": "Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.",
"validationPinRequired": "Wprowadź PIN", "authFailed": "Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.",
"validationPinLength": "PIN musi mieć 6 cyfr",
"connectionTimedOut": "Upłynął czas połączenia. Sprawdź ID sesji i PIN.",
"sessionNotFound": "Nie znaleziono sesji. Sprawdź swoje dane.",
"failedToConnect": "Nie udało się połączyć: ${error}" "failedToConnect": "Nie udało się połączyć: ${error}"
}, },
"remote": { "remote": {
+27 -37
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Servidor de controlo remoto",
"companionRemoteServerDescription": "Permitir que dispositivos móveis na sua rede controlem esta aplicação"
}, },
"search": { "search": {
"hint": "Buscar filmes, séries, músicas...", "hint": "Buscar filmes, séries, músicas...",
@@ -778,47 +780,35 @@
"controlDesktop": "Controlar um dispositivo desktop", "controlDesktop": "Controlar um dispositivo desktop",
"connectedTo": "Conectado a ${name}", "connectedTo": "Conectado a ${name}",
"session": { "session": {
"creatingSession": "Criando sessão remota...", "startingServer": "A iniciar servidor remoto...",
"failedToCreate": "Falha ao criar sessão remota:", "failedToCreate": "Falha ao iniciar o servidor remoto:",
"noSession": "Nenhuma sessão disponível", "hostAddress": "Endereço do host",
"scanQrCode": "Escanear QR Code",
"orEnterManually": "Ou insira manualmente",
"hostAddress": "Endereço do Host",
"sessionId": "ID da Sessão",
"pin": "PIN",
"connected": "Conectado", "connected": "Conectado",
"waitingForConnection": "Aguardando conexão...", "serverRunning": "Servidor remoto ativo",
"usePhoneToControl": "Use seu dispositivo móvel para controlar este app", "serverStopped": "Servidor remoto parado",
"copiedToClipboard": "${label} copiado para a área de transferência", "serverRunningDescription": "Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação",
"copyToClipboard": "Copiar para área de transferência", "serverStoppedDescription": "Inicie o servidor para permitir que dispositivos móveis se conectem",
"newSession": "Nova Sessão", "usePhoneToControl": "Use o seu dispositivo móvel para controlar esta aplicação",
"startServer": "Iniciar servidor",
"stopServer": "Parar servidor",
"minimize": "Minimizar" "minimize": "Minimizar"
}, },
"pairing": { "pairing": {
"scan": "Escanear", "pairWithDesktop": "Conectar ao desktop",
"manual": "Manual", "discoveryDescription": "Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente",
"pairWithDesktop": "Parear com Desktop",
"enterSessionDetails": "Insira os detalhes da sessão exibidos no seu dispositivo desktop",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Insira o ID da sessão de 8 caracteres", "connecting": "A conectar...",
"pinHint": "Insira o PIN de 6 dígitos", "searchingForDevices": "A procurar dispositivos...",
"connecting": "Conectando...", "noDevicesFound": "Nenhum dispositivo encontrado na sua rede",
"tips": "Dicas", "noDevicesHint": "Certifique-se de que o Plezy está aberto no seu desktop e que ambos os dispositivos estão na mesma rede WiFi",
"tipDesktop": "Abra o Plezy no desktop e ative o Controle Remoto nas configurações ou menu", "availableDevices": "Dispositivos disponíveis",
"tipScan": "Use a aba Escanear para parear rapidamente escaneando o QR code no seu desktop", "manualConnection": "Conexão manual",
"tipWifi": "Certifique-se de que ambos os dispositivos estão na mesma rede WiFi", "cryptoInitFailed": "Não foi possível inicializar a conexão segura. Certifique-se de que está conectado a uma conta Plex.",
"cameraPermissionRequired": "A permissão da câmera é necessária para escanear QR codes.\nConceda acesso à câmera nas configurações do dispositivo.", "validationHostRequired": "Introduza o endereço do host",
"cameraError": "Não foi possível iniciar a câmera: ${error}", "validationHostFormat": "O formato deve ser IP:porta (ex. 192.168.1.100:48632)",
"scanInstruction": "Aponte sua câmera para o QR code exibido no seu desktop", "connectionTimedOut": "Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.",
"invalidQrCode": "Formato de QR code inválido", "sessionNotFound": "Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.",
"validationHostRequired": "Insira o endereço do host", "authFailed": "Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.",
"validationHostFormat": "O formato deve ser IP:porta (ex: 192.168.1.100:48632)",
"validationSessionIdRequired": "Insira um ID de sessão",
"validationSessionIdLength": "O ID da sessão deve ter 8 caracteres",
"validationPinRequired": "Insira um PIN",
"validationPinLength": "O PIN deve ter 6 dígitos",
"connectionTimedOut": "Tempo de conexão esgotado. Verifique o ID da sessão e o PIN.",
"sessionNotFound": "Sessão não encontrada. Verifique suas credenciais.",
"failedToConnect": "Falha ao conectar: ${error}" "failedToConnect": "Falha ao conectar: ${error}"
}, },
"remote": { "remote": {
+22 -32
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Сервер удалённого управления",
"companionRemoteServerDescription": "Разрешить мобильным устройствам в сети управлять этим приложением"
}, },
"search": { "search": {
"hint": "Поиск фильмов, сериалов, музыки...", "hint": "Поиск фильмов, сериалов, музыки...",
@@ -778,47 +780,35 @@
"controlDesktop": "Управлять десктопным устройством", "controlDesktop": "Управлять десктопным устройством",
"connectedTo": "Подключено к ${name}", "connectedTo": "Подключено к ${name}",
"session": { "session": {
"creatingSession": "Создание удалённой сессии...", "startingServer": "Запуск удалённого сервера...",
"failedToCreate": "Не удалось создать удалённую сессию:", "failedToCreate": "Не удалось запустить удалённый сервер:",
"noSession": "Нет доступных сессий",
"scanQrCode": "Сканировать QR-код",
"orEnterManually": "Или введите вручную",
"hostAddress": "Адрес хоста", "hostAddress": "Адрес хоста",
"sessionId": "ID сессии",
"pin": "PIN",
"connected": "Подключено", "connected": "Подключено",
"waitingForConnection": "Ожидание подключения...", "serverRunning": "Удалённый сервер активен",
"serverStopped": "Удалённый сервер остановлен",
"serverRunningDescription": "Мобильные устройства в вашей сети могут обнаруживать это приложение и подключаться к нему",
"serverStoppedDescription": "Запустите сервер, чтобы разрешить подключение мобильных устройств",
"usePhoneToControl": "Используйте мобильное устройство для управления этим приложением", "usePhoneToControl": "Используйте мобильное устройство для управления этим приложением",
"copiedToClipboard": "${label} скопировано в буфер обмена", "startServer": "Запустить сервер",
"copyToClipboard": "Скопировать в буфер обмена", "stopServer": "Остановить сервер",
"newSession": "Новая сессия",
"minimize": "Свернуть" "minimize": "Свернуть"
}, },
"pairing": { "pairing": {
"scan": "Сканировать", "pairWithDesktop": "Подключиться к компьютеру",
"manual": "Вручную", "discoveryDescription": "Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически",
"pairWithDesktop": "Сопряжение с ПК",
"enterSessionDetails": "Введите данные сессии, показанные на десктопном устройстве",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Введите 8-символьный ID сессии",
"pinHint": "Введите 6-значный PIN",
"connecting": "Подключение...", "connecting": "Подключение...",
"tips": "Советы", "searchingForDevices": "Поиск устройств...",
"tipDesktop": "Откройте Plezy на десктопе и включите пульт управления в настройках или меню", "noDevicesFound": "Устройства в вашей сети не найдены",
"tipScan": "Используйте вкладку «Сканировать» для быстрого сопряжения через QR-код на десктопе", "noDevicesHint": "Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi",
"tipWifi": "Убедитесь, что оба устройства находятся в одной WiFi-сети", "availableDevices": "Доступные устройства",
"cameraPermissionRequired": "Для сканирования QR-кодов требуется разрешение на камеру.\nПредоставьте доступ к камере в настройках устройства.", "manualConnection": "Ручное подключение",
"cameraError": "Не удалось запустить камеру: ${error}", "cryptoInitFailed": "Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.",
"scanInstruction": "Наведите камеру на QR-код, показанный на десктопе",
"invalidQrCode": "Недопустимый формат QR-кода",
"validationHostRequired": "Введите адрес хоста", "validationHostRequired": "Введите адрес хоста",
"validationHostFormat": "Формат должен быть IP:порт (например, 192.168.1.100:48632)", "validationHostFormat": "Формат должен быть IP:порт (например, 192.168.1.100:48632)",
"validationSessionIdRequired": "Введите ID сессии", "connectionTimedOut": "Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.",
"validationSessionIdLength": "ID сессии должен содержать 8 символов", "sessionNotFound": "Устройство не найдено. Убедитесь, что Plezy запущен на хосте.",
"validationPinRequired": "Введите PIN", "authFailed": "Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.",
"validationPinLength": "PIN должен содержать 6 цифр",
"connectionTimedOut": "Время подключения истекло. Проверьте ID сессии и PIN.",
"sessionNotFound": "Сессия не найдена. Проверьте ваши учётные данные.",
"failedToConnect": "Не удалось подключиться: ${error}" "failedToConnect": "Не удалось подключиться: ${error}"
}, },
"remote": { "remote": {
+2 -2
View File
@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 15 /// Locales: 15
/// Strings: 13110 (874 per locale) /// Strings: 12960 (864 per locale)
/// ///
/// Built on 2026-04-13 at 20:34 UTC /// Built on 2026-04-14 at 10:29 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+52 -72
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsDa implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Companion Remote Server';
@override String get companionRemoteServerDescription => 'Tillad mobilenheder på dit netværk at styre denne app';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionDa implements TranslationsCompanionRemo
final TranslationsDa _root; // ignore: unused_field final TranslationsDa _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Opretter fjernsession...'; @override String get startingServer => 'Starter fjernserver...';
@override String get failedToCreate => 'Kunne ikke oprette fjernsession:'; @override String get failedToCreate => 'Kunne ikke starte fjernserver:';
@override String get noSession => 'Ingen session tilgængelig'; @override String get hostAddress => 'Værtsadresse';
@override String get scanQrCode => 'Scan QR-kode';
@override String get orEnterManually => 'Eller indtast manuelt';
@override String get hostAddress => 'Værtadresse';
@override String get sessionId => 'Sessions-ID';
@override String get pin => 'PIN';
@override String get connected => 'Forbundet'; @override String get connected => 'Forbundet';
@override String get waitingForConnection => 'Venter på forbindelse...'; @override String get serverRunning => 'Fjernserver aktiv';
@override String get serverStopped => 'Fjernserver stoppet';
@override String get serverRunningDescription => 'Mobilenheder på dit netværk kan finde og oprette forbindelse til denne app';
@override String get serverStoppedDescription => 'Start serveren for at tillade mobilenheder at oprette forbindelse';
@override String get usePhoneToControl => 'Brug din mobilenhed til at styre denne app'; @override String get usePhoneToControl => 'Brug din mobilenhed til at styre denne app';
@override String copiedToClipboard({required Object label}) => '${label} kopieret til udklipsholder'; @override String get startServer => 'Start server';
@override String get copyToClipboard => 'Kopiér til udklipsholder'; @override String get stopServer => 'Stop server';
@override String get newSession => 'Ny session';
@override String get minimize => 'Minimér'; @override String get minimize => 'Minimér';
} }
@@ -1298,31 +1297,22 @@ class _TranslationsCompanionRemotePairingDa implements TranslationsCompanionRemo
final TranslationsDa _root; // ignore: unused_field final TranslationsDa _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Scan'; @override String get pairWithDesktop => 'Opret forbindelse til computer';
@override String get manual => 'Manuel'; @override String get discoveryDescription => 'Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk';
@override String get pairWithDesktop => 'Par med desktop';
@override String get enterSessionDetails => 'Indtast sessionsdetaljerne vist på din desktopenhed';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Indtast 8-tegns sessions-ID'; @override String get connecting => 'Opretter forbindelse...';
@override String get pinHint => 'Indtast 6-cifret PIN'; @override String get searchingForDevices => 'Søger efter enheder...';
@override String get connecting => 'Forbinder...'; @override String get noDevicesFound => 'Ingen enheder fundet på dit netværk';
@override String get tips => 'Tips'; @override String get noDevicesHint => 'Sørg for, at Plezy er åben på din computer, og at begge enheder er på det samme WiFi-netværk';
@override String get tipDesktop => 'Åbn Plezy på din desktop og aktivér fjernbetjening fra indstillinger eller menu'; @override String get availableDevices => 'Tilgængelige enheder';
@override String get tipScan => 'Brug fanen Scan til hurtigt at parre ved at scanne QR-koden på din desktop'; @override String get manualConnection => 'Manuel forbindelse';
@override String get tipWifi => 'Sørg for, at begge enheder er på det samme WiFi-netværk'; @override String get cryptoInitFailed => 'Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.';
@override String get cameraPermissionRequired => 'Kameratilladelse er påkrævet for at scanne QR-koder.\nGiv kameraadgang i enhedsindstillinger.'; @override String get validationHostRequired => 'Angiv venligst værtsadresse';
@override String cameraError({required Object error}) => 'Kunne ikke starte kamera: ${error}';
@override String get scanInstruction => 'Ret kameraet mod QR-koden vist på din desktop';
@override String get invalidQrCode => 'Ugyldigt QR-kodeformat';
@override String get validationHostRequired => 'Indtast værtadresse';
@override String get validationHostFormat => 'Format skal være IP:port (f.eks. 192.168.1.100:48632)'; @override String get validationHostFormat => 'Format skal være IP:port (f.eks. 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Indtast et sessions-ID'; @override String get connectionTimedOut => 'Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.';
@override String get validationSessionIdLength => 'Sessions-ID skal være 8 tegn'; @override String get sessionNotFound => 'Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.';
@override String get validationPinRequired => 'Indtast en PIN'; @override String get authFailed => 'Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.';
@override String get validationPinLength => 'PIN skal være 6 cifre'; @override String failedToConnect({required Object error}) => 'Kunne ikke oprette forbindelse: ${error}';
@override String get connectionTimedOut => 'Forbindelsen fik timeout. Tjek sessions-ID og PIN.';
@override String get sessionNotFound => 'Kunne ikke finde sessionen. Tjek dine oplysninger.';
@override String failedToConnect({required Object error}) => 'Kunne ikke forbinde: ${error}';
} }
// Path: companionRemote.remote // Path: companionRemote.remote
@@ -1600,6 +1590,8 @@ extension on TranslationsDa {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Companion Remote Server',
'settings.companionRemoteServerDescription' => 'Tillad mobilenheder på dit netværk at styre denne app',
'search.hint' => 'Søg film, serier, musik...', 'search.hint' => 'Søg film, serier, musik...',
'search.tryDifferentTerm' => 'Prøv en anden søgning', 'search.tryDifferentTerm' => 'Prøv en anden søgning',
'search.searchYourMedia' => 'Søg i dine medier', 'search.searchYourMedia' => 'Søg i dine medier',
@@ -1881,10 +1873,10 @@ extension on TranslationsDa {
'libraries.groupings.seasons' => 'Sæsoner', 'libraries.groupings.seasons' => 'Sæsoner',
'libraries.groupings.episodes' => 'Episoder', 'libraries.groupings.episodes' => 'Episoder',
'libraries.groupings.folders' => 'Mapper', 'libraries.groupings.folders' => 'Mapper',
'about.title' => 'Om',
'about.openSourceLicenses' => 'Open source-licenser',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Om',
'about.openSourceLicenses' => 'Open source-licenser',
'about.versionLabel' => ({required Object version}) => 'Version ${version}', 'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'En smuk Plex-klient til Flutter', 'about.appDescription' => 'En smuk Plex-klient til Flutter',
'about.viewLicensesDescription' => 'Se licenser for tredjepartsbiblioteker', 'about.viewLicensesDescription' => 'Se licenser for tredjepartsbiblioteker',
@@ -2076,46 +2068,34 @@ extension on TranslationsDa {
'companionRemote.remoteControl' => 'Fjernbetjening', 'companionRemote.remoteControl' => 'Fjernbetjening',
'companionRemote.controlDesktop' => 'Styr en desktopenhed', 'companionRemote.controlDesktop' => 'Styr en desktopenhed',
'companionRemote.connectedTo' => ({required Object name}) => 'Forbundet til ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Forbundet til ${name}',
'companionRemote.session.creatingSession' => 'Opretter fjernsession...', 'companionRemote.session.startingServer' => 'Starter fjernserver...',
'companionRemote.session.failedToCreate' => 'Kunne ikke oprette fjernsession:', 'companionRemote.session.failedToCreate' => 'Kunne ikke starte fjernserver:',
'companionRemote.session.noSession' => 'Ingen session tilgængelig', 'companionRemote.session.hostAddress' => 'Værtsadresse',
'companionRemote.session.scanQrCode' => 'Scan QR-kode',
'companionRemote.session.orEnterManually' => 'Eller indtast manuelt',
'companionRemote.session.hostAddress' => 'Værtadresse',
'companionRemote.session.sessionId' => 'Sessions-ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Forbundet', 'companionRemote.session.connected' => 'Forbundet',
'companionRemote.session.waitingForConnection' => 'Venter på forbindelse...', 'companionRemote.session.serverRunning' => 'Fjernserver aktiv',
'companionRemote.session.serverStopped' => 'Fjernserver stoppet',
'companionRemote.session.serverRunningDescription' => 'Mobilenheder på dit netværk kan finde og oprette forbindelse til denne app',
'companionRemote.session.serverStoppedDescription' => 'Start serveren for at tillade mobilenheder at oprette forbindelse',
'companionRemote.session.usePhoneToControl' => 'Brug din mobilenhed til at styre denne app', 'companionRemote.session.usePhoneToControl' => 'Brug din mobilenhed til at styre denne app',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} kopieret til udklipsholder', 'companionRemote.session.startServer' => 'Start server',
'companionRemote.session.copyToClipboard' => 'Kopiér til udklipsholder', 'companionRemote.session.stopServer' => 'Stop server',
'companionRemote.session.newSession' => 'Ny session',
'companionRemote.session.minimize' => 'Minimér', 'companionRemote.session.minimize' => 'Minimér',
'companionRemote.pairing.scan' => 'Scan', 'companionRemote.pairing.pairWithDesktop' => 'Opret forbindelse til computer',
'companionRemote.pairing.manual' => 'Manuel', 'companionRemote.pairing.discoveryDescription' => 'Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk',
'companionRemote.pairing.pairWithDesktop' => 'Par med desktop',
'companionRemote.pairing.enterSessionDetails' => 'Indtast sessionsdetaljerne vist på din desktopenhed',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Indtast 8-tegns sessions-ID', 'companionRemote.pairing.connecting' => 'Opretter forbindelse...',
'companionRemote.pairing.pinHint' => 'Indtast 6-cifret PIN', 'companionRemote.pairing.searchingForDevices' => 'Søger efter enheder...',
'companionRemote.pairing.connecting' => 'Forbinder...', 'companionRemote.pairing.noDevicesFound' => 'Ingen enheder fundet på dit netværk',
'companionRemote.pairing.tips' => 'Tips', 'companionRemote.pairing.noDevicesHint' => 'Sørg for, at Plezy er åben på din computer, og at begge enheder er på det samme WiFi-netværk',
'companionRemote.pairing.tipDesktop' => 'Åbn Plezy på din desktop og aktivér fjernbetjening fra indstillinger eller menu', 'companionRemote.pairing.availableDevices' => 'Tilgængelige enheder',
'companionRemote.pairing.tipScan' => 'Brug fanen Scan til hurtigt at parre ved at scanne QR-koden på din desktop', 'companionRemote.pairing.manualConnection' => 'Manuel forbindelse',
'companionRemote.pairing.tipWifi' => 'Sørg for, at begge enheder er på det samme WiFi-netværk', 'companionRemote.pairing.cryptoInitFailed' => 'Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.',
'companionRemote.pairing.cameraPermissionRequired' => 'Kameratilladelse er påkrævet for at scanne QR-koder.\nGiv kameraadgang i enhedsindstillinger.', 'companionRemote.pairing.validationHostRequired' => 'Angiv venligst værtsadresse',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kunne ikke starte kamera: ${error}',
'companionRemote.pairing.scanInstruction' => 'Ret kameraet mod QR-koden vist på din desktop',
'companionRemote.pairing.invalidQrCode' => 'Ugyldigt QR-kodeformat',
'companionRemote.pairing.validationHostRequired' => 'Indtast værtadresse',
'companionRemote.pairing.validationHostFormat' => 'Format skal være IP:port (f.eks. 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Format skal være IP:port (f.eks. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Indtast et sessions-ID', 'companionRemote.pairing.connectionTimedOut' => 'Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.',
'companionRemote.pairing.validationSessionIdLength' => 'Sessions-ID skal være 8 tegn', 'companionRemote.pairing.sessionNotFound' => 'Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.',
'companionRemote.pairing.validationPinRequired' => 'Indtast en PIN', 'companionRemote.pairing.authFailed' => 'Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.',
'companionRemote.pairing.validationPinLength' => 'PIN skal være 6 cifre', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke oprette forbindelse: ${error}',
'companionRemote.pairing.connectionTimedOut' => 'Forbindelsen fik timeout. Tjek sessions-ID og PIN.',
'companionRemote.pairing.sessionNotFound' => 'Kunne ikke finde sessionen. Tjek dine oplysninger.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke forbinde: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vil du afbryde fra fjernsessionen?', 'companionRemote.remote.disconnectConfirm' => 'Vil du afbryde fra fjernsessionen?',
'companionRemote.remote.reconnecting' => 'Genopretter forbindelse...', 'companionRemote.remote.reconnecting' => 'Genopretter forbindelse...',
'companionRemote.remote.attemptOf' => ({required Object current}) => 'Forsøg ${current} af 5', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Forsøg ${current} af 5',
+46 -66
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Companion Remote Server';
@override String get companionRemoteServerDescription => 'Mobilgeräten in deinem Netzwerk erlauben, diese App zu steuern';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionDe implements TranslationsCompanionRemo
final TranslationsDe _root; // ignore: unused_field final TranslationsDe _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Fernsteuerungssitzung wird erstellt...'; @override String get startingServer => 'Remote-Server wird gestartet...';
@override String get failedToCreate => 'Fernsteuerungssitzung konnte nicht erstellt werden:'; @override String get failedToCreate => 'Remote-Server konnte nicht gestartet werden:';
@override String get noSession => 'Keine Sitzung verfügbar';
@override String get scanQrCode => 'QR-Code scannen';
@override String get orEnterManually => 'Oder manuell eingeben';
@override String get hostAddress => 'Host-Adresse'; @override String get hostAddress => 'Host-Adresse';
@override String get sessionId => 'Sitzungs-ID';
@override String get pin => 'PIN';
@override String get connected => 'Verbunden'; @override String get connected => 'Verbunden';
@override String get waitingForConnection => 'Warte auf Verbindung...'; @override String get serverRunning => 'Remote-Server aktiv';
@override String get serverStopped => 'Remote-Server gestoppt';
@override String get serverRunningDescription => 'Mobilgeräte in deinem Netzwerk können diese App finden und sich verbinden';
@override String get serverStoppedDescription => 'Starte den Server, um Mobilgeräten die Verbindung zu ermöglichen';
@override String get usePhoneToControl => 'Verwende dein Mobilgerät, um diese App zu steuern'; @override String get usePhoneToControl => 'Verwende dein Mobilgerät, um diese App zu steuern';
@override String copiedToClipboard({required Object label}) => '${label} in Zwischenablage kopiert'; @override String get startServer => 'Server starten';
@override String get copyToClipboard => 'In Zwischenablage kopieren'; @override String get stopServer => 'Server stoppen';
@override String get newSession => 'Neue Sitzung';
@override String get minimize => 'Minimieren'; @override String get minimize => 'Minimieren';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingDe implements TranslationsCompanionRemo
final TranslationsDe _root; // ignore: unused_field final TranslationsDe _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Scannen'; @override String get pairWithDesktop => 'Mit Desktop verbinden';
@override String get manual => 'Manuell'; @override String get discoveryDescription => 'Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch';
@override String get pairWithDesktop => 'Mit Desktop koppeln';
@override String get enterSessionDetails => 'Gib die Sitzungsdetails ein, die auf deinem Desktop-Gerät angezeigt werden';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => '8-stellige Sitzungs-ID eingeben'; @override String get connecting => 'Verbinden...';
@override String get pinHint => '6-stellige PIN eingeben'; @override String get searchingForDevices => 'Suche nach Geräten...';
@override String get connecting => 'Verbinde...'; @override String get noDevicesFound => 'Keine Geräte in deinem Netzwerk gefunden';
@override String get tips => 'Tipps'; @override String get noDevicesHint => 'Stelle sicher, dass Plezy auf deinem Desktop geöffnet ist und beide Geräte im selben WLAN sind';
@override String get tipDesktop => 'Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü'; @override String get availableDevices => 'Verfügbare Geräte';
@override String get tipScan => 'Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln'; @override String get manualConnection => 'Manuelle Verbindung';
@override String get tipWifi => 'Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind'; @override String get cryptoInitFailed => 'Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.';
@override String get cameraPermissionRequired => 'Kameraberechtigung wird zum Scannen von QR-Codes benötigt.\nBitte erteile den Kamerazugriff in den Geräteeinstellungen.';
@override String cameraError({required Object error}) => 'Kamera konnte nicht gestartet werden: ${error}';
@override String get scanInstruction => 'Richte deine Kamera auf den QR-Code auf deinem Desktop';
@override String get invalidQrCode => 'Ungültiges QR-Code-Format';
@override String get validationHostRequired => 'Bitte Host-Adresse eingeben'; @override String get validationHostRequired => 'Bitte Host-Adresse eingeben';
@override String get validationHostFormat => 'Format muss IP:Port sein (z.B. 192.168.1.100:48632)'; @override String get validationHostFormat => 'Format muss IP:Port sein (z.B. 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Bitte Sitzungs-ID eingeben'; @override String get connectionTimedOut => 'Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.';
@override String get validationSessionIdLength => 'Sitzungs-ID muss 8 Zeichen lang sein'; @override String get sessionNotFound => 'Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.';
@override String get validationPinRequired => 'Bitte PIN eingeben'; @override String get authFailed => 'Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.';
@override String get validationPinLength => 'PIN muss 6 Ziffern haben';
@override String get connectionTimedOut => 'Zeitüberschreitung. Bitte Sitzungs-ID und PIN überprüfen.';
@override String get sessionNotFound => 'Sitzung nicht gefunden. Bitte Zugangsdaten überprüfen.';
@override String failedToConnect({required Object error}) => 'Verbindung fehlgeschlagen: ${error}'; @override String failedToConnect({required Object error}) => 'Verbindung fehlgeschlagen: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsDe {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Companion Remote Server',
'settings.companionRemoteServerDescription' => 'Mobilgeräten in deinem Netzwerk erlauben, diese App zu steuern',
'search.hint' => 'Filme, Serien, Musik suchen...', 'search.hint' => 'Filme, Serien, Musik suchen...',
'search.tryDifferentTerm' => 'Anderen Suchbegriff versuchen', 'search.tryDifferentTerm' => 'Anderen Suchbegriff versuchen',
'search.searchYourMedia' => 'In den eigenen Medien suchen', 'search.searchYourMedia' => 'In den eigenen Medien suchen',
@@ -1881,10 +1873,10 @@ extension on TranslationsDe {
'libraries.groupings.seasons' => 'Staffeln', 'libraries.groupings.seasons' => 'Staffeln',
'libraries.groupings.episodes' => 'Episoden', 'libraries.groupings.episodes' => 'Episoden',
'libraries.groupings.folders' => 'Ordner', 'libraries.groupings.folders' => 'Ordner',
'about.title' => 'Über',
'about.openSourceLicenses' => 'Open-Source-Lizenzen',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Über',
'about.openSourceLicenses' => 'Open-Source-Lizenzen',
'about.versionLabel' => ({required Object version}) => 'Version ${version}', 'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'Ein schöner Plex-Client für Flutter', 'about.appDescription' => 'Ein schöner Plex-Client für Flutter',
'about.viewLicensesDescription' => 'Lizenzen von Drittanbieter-Bibliotheken anzeigen', 'about.viewLicensesDescription' => 'Lizenzen von Drittanbieter-Bibliotheken anzeigen',
@@ -2076,45 +2068,33 @@ extension on TranslationsDe {
'companionRemote.remoteControl' => 'Fernsteuerung', 'companionRemote.remoteControl' => 'Fernsteuerung',
'companionRemote.controlDesktop' => 'Ein Desktop-Gerät steuern', 'companionRemote.controlDesktop' => 'Ein Desktop-Gerät steuern',
'companionRemote.connectedTo' => ({required Object name}) => 'Verbunden mit ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Verbunden mit ${name}',
'companionRemote.session.creatingSession' => 'Fernsteuerungssitzung wird erstellt...', 'companionRemote.session.startingServer' => 'Remote-Server wird gestartet...',
'companionRemote.session.failedToCreate' => 'Fernsteuerungssitzung konnte nicht erstellt werden:', 'companionRemote.session.failedToCreate' => 'Remote-Server konnte nicht gestartet werden:',
'companionRemote.session.noSession' => 'Keine Sitzung verfügbar',
'companionRemote.session.scanQrCode' => 'QR-Code scannen',
'companionRemote.session.orEnterManually' => 'Oder manuell eingeben',
'companionRemote.session.hostAddress' => 'Host-Adresse', 'companionRemote.session.hostAddress' => 'Host-Adresse',
'companionRemote.session.sessionId' => 'Sitzungs-ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Verbunden', 'companionRemote.session.connected' => 'Verbunden',
'companionRemote.session.waitingForConnection' => 'Warte auf Verbindung...', 'companionRemote.session.serverRunning' => 'Remote-Server aktiv',
'companionRemote.session.serverStopped' => 'Remote-Server gestoppt',
'companionRemote.session.serverRunningDescription' => 'Mobilgeräte in deinem Netzwerk können diese App finden und sich verbinden',
'companionRemote.session.serverStoppedDescription' => 'Starte den Server, um Mobilgeräten die Verbindung zu ermöglichen',
'companionRemote.session.usePhoneToControl' => 'Verwende dein Mobilgerät, um diese App zu steuern', 'companionRemote.session.usePhoneToControl' => 'Verwende dein Mobilgerät, um diese App zu steuern',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} in Zwischenablage kopiert', 'companionRemote.session.startServer' => 'Server starten',
'companionRemote.session.copyToClipboard' => 'In Zwischenablage kopieren', 'companionRemote.session.stopServer' => 'Server stoppen',
'companionRemote.session.newSession' => 'Neue Sitzung',
'companionRemote.session.minimize' => 'Minimieren', 'companionRemote.session.minimize' => 'Minimieren',
'companionRemote.pairing.scan' => 'Scannen', 'companionRemote.pairing.pairWithDesktop' => 'Mit Desktop verbinden',
'companionRemote.pairing.manual' => 'Manuell', 'companionRemote.pairing.discoveryDescription' => 'Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch',
'companionRemote.pairing.pairWithDesktop' => 'Mit Desktop koppeln',
'companionRemote.pairing.enterSessionDetails' => 'Gib die Sitzungsdetails ein, die auf deinem Desktop-Gerät angezeigt werden',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '8-stellige Sitzungs-ID eingeben', 'companionRemote.pairing.connecting' => 'Verbinden...',
'companionRemote.pairing.pinHint' => '6-stellige PIN eingeben', 'companionRemote.pairing.searchingForDevices' => 'Suche nach Geräten...',
'companionRemote.pairing.connecting' => 'Verbinde...', 'companionRemote.pairing.noDevicesFound' => 'Keine Geräte in deinem Netzwerk gefunden',
'companionRemote.pairing.tips' => 'Tipps', 'companionRemote.pairing.noDevicesHint' => 'Stelle sicher, dass Plezy auf deinem Desktop geöffnet ist und beide Geräte im selben WLAN sind',
'companionRemote.pairing.tipDesktop' => 'Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü', 'companionRemote.pairing.availableDevices' => 'Verfügbare Geräte',
'companionRemote.pairing.tipScan' => 'Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln', 'companionRemote.pairing.manualConnection' => 'Manuelle Verbindung',
'companionRemote.pairing.tipWifi' => 'Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind', 'companionRemote.pairing.cryptoInitFailed' => 'Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.',
'companionRemote.pairing.cameraPermissionRequired' => 'Kameraberechtigung wird zum Scannen von QR-Codes benötigt.\nBitte erteile den Kamerazugriff in den Geräteeinstellungen.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kamera konnte nicht gestartet werden: ${error}',
'companionRemote.pairing.scanInstruction' => 'Richte deine Kamera auf den QR-Code auf deinem Desktop',
'companionRemote.pairing.invalidQrCode' => 'Ungültiges QR-Code-Format',
'companionRemote.pairing.validationHostRequired' => 'Bitte Host-Adresse eingeben', 'companionRemote.pairing.validationHostRequired' => 'Bitte Host-Adresse eingeben',
'companionRemote.pairing.validationHostFormat' => 'Format muss IP:Port sein (z.B. 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Format muss IP:Port sein (z.B. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Bitte Sitzungs-ID eingeben', 'companionRemote.pairing.connectionTimedOut' => 'Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.',
'companionRemote.pairing.validationSessionIdLength' => 'Sitzungs-ID muss 8 Zeichen lang sein', 'companionRemote.pairing.sessionNotFound' => 'Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.',
'companionRemote.pairing.validationPinRequired' => 'Bitte PIN eingeben', 'companionRemote.pairing.authFailed' => 'Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.',
'companionRemote.pairing.validationPinLength' => 'PIN muss 6 Ziffern haben',
'companionRemote.pairing.connectionTimedOut' => 'Zeitüberschreitung. Bitte Sitzungs-ID und PIN überprüfen.',
'companionRemote.pairing.sessionNotFound' => 'Sitzung nicht gefunden. Bitte Zugangsdaten überprüfen.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Verbindung fehlgeschlagen: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Verbindung fehlgeschlagen: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Möchtest du die Verbindung zur Fernsteuerungssitzung trennen?', 'companionRemote.remote.disconnectConfirm' => 'Möchtest du die Verbindung zur Fernsteuerungssitzung trennen?',
'companionRemote.remote.reconnecting' => 'Verbindung wird wiederhergestellt...', 'companionRemote.remote.reconnecting' => 'Verbindung wird wiederhergestellt...',
+70 -110
View File
@@ -730,6 +730,12 @@ class TranslationsSettingsEn {
/// en: 'Show what you're watching on Discord' /// en: 'Show what you're watching on Discord'
String get discordRichPresenceDescription => 'Show what you\'re watching on Discord'; String get discordRichPresenceDescription => 'Show what you\'re watching on Discord';
/// en: 'Companion Remote Server'
String get companionRemoteServer => 'Companion Remote Server';
/// en: 'Allow mobile devices on your network to control this app'
String get companionRemoteServerDescription => 'Allow mobile devices on your network to control this app';
/// en: 'Auto Picture-in-Picture' /// en: 'Auto Picture-in-Picture'
String get autoPip => 'Auto Picture-in-Picture'; String get autoPip => 'Auto Picture-in-Picture';
@@ -2892,47 +2898,38 @@ class TranslationsCompanionRemoteSessionEn {
// Translations // Translations
/// en: 'Creating remote session...' /// en: 'Starting remote server...'
String get creatingSession => 'Creating remote session...'; String get startingServer => 'Starting remote server...';
/// en: 'Failed to create remote session:' /// en: 'Failed to start remote server:'
String get failedToCreate => 'Failed to create remote session:'; String get failedToCreate => 'Failed to start remote server:';
/// en: 'No session available'
String get noSession => 'No session available';
/// en: 'Scan QR Code'
String get scanQrCode => 'Scan QR Code';
/// en: 'Or enter manually'
String get orEnterManually => 'Or enter manually';
/// en: 'Host Address' /// en: 'Host Address'
String get hostAddress => 'Host Address'; String get hostAddress => 'Host Address';
/// en: 'Session ID'
String get sessionId => 'Session ID';
/// en: 'PIN'
String get pin => 'PIN';
/// en: 'Connected' /// en: 'Connected'
String get connected => 'Connected'; String get connected => 'Connected';
/// en: 'Waiting for connection...' /// en: 'Remote server active'
String get waitingForConnection => 'Waiting for connection...'; String get serverRunning => 'Remote server active';
/// en: 'Remote server stopped'
String get serverStopped => 'Remote server stopped';
/// en: 'Mobile devices on your network can discover and connect to this app'
String get serverRunningDescription => 'Mobile devices on your network can discover and connect to this app';
/// en: 'Start the server to allow mobile devices to connect'
String get serverStoppedDescription => 'Start the server to allow mobile devices to connect';
/// en: 'Use your mobile device to control this app' /// en: 'Use your mobile device to control this app'
String get usePhoneToControl => 'Use your mobile device to control this app'; String get usePhoneToControl => 'Use your mobile device to control this app';
/// en: '${label} copied to clipboard' /// en: 'Start Server'
String copiedToClipboard({required Object label}) => '${label} copied to clipboard'; String get startServer => 'Start Server';
/// en: 'Copy to clipboard' /// en: 'Stop Server'
String get copyToClipboard => 'Copy to clipboard'; String get stopServer => 'Stop Server';
/// en: 'New Session'
String get newSession => 'New Session';
/// en: 'Minimize' /// en: 'Minimize'
String get minimize => 'Minimize'; String get minimize => 'Minimize';
@@ -2946,53 +2943,35 @@ class TranslationsCompanionRemotePairingEn {
// Translations // Translations
/// en: 'Scan' /// en: 'Connect to Desktop'
String get scan => 'Scan'; String get pairWithDesktop => 'Connect to Desktop';
/// en: 'Manual' /// en: 'Devices on your network running Plezy with the same Plex account will appear automatically'
String get manual => 'Manual'; String get discoveryDescription => 'Devices on your network running Plezy with the same Plex account will appear automatically';
/// en: 'Pair with Desktop'
String get pairWithDesktop => 'Pair with Desktop';
/// en: 'Enter the session details shown on your desktop device'
String get enterSessionDetails => 'Enter the session details shown on your desktop device';
/// en: '192.168.1.100:48632' /// en: '192.168.1.100:48632'
String get hostAddressHint => '192.168.1.100:48632'; String get hostAddressHint => '192.168.1.100:48632';
/// en: 'Enter 8-character session ID'
String get sessionIdHint => 'Enter 8-character session ID';
/// en: 'Enter 6-digit PIN'
String get pinHint => 'Enter 6-digit PIN';
/// en: 'Connecting...' /// en: 'Connecting...'
String get connecting => 'Connecting...'; String get connecting => 'Connecting...';
/// en: 'Tips' /// en: 'Looking for devices...'
String get tips => 'Tips'; String get searchingForDevices => 'Looking for devices...';
/// en: 'Open Plezy on your desktop and enable Companion Remote from settings or menu' /// en: 'No devices found on your network'
String get tipDesktop => 'Open Plezy on your desktop and enable Companion Remote from settings or menu'; String get noDevicesFound => 'No devices found on your network';
/// en: 'Use the Scan tab to quickly pair by scanning the QR code on your desktop' /// en: 'Make sure Plezy is open on your desktop and both devices are on the same WiFi network'
String get tipScan => 'Use the Scan tab to quickly pair by scanning the QR code on your desktop'; String get noDevicesHint => 'Make sure Plezy is open on your desktop and both devices are on the same WiFi network';
/// en: 'Make sure both devices are on the same WiFi network' /// en: 'Available Devices'
String get tipWifi => 'Make sure both devices are on the same WiFi network'; String get availableDevices => 'Available Devices';
/// en: 'Camera permission is required to scan QR codes. Please grant camera access in your device settings.' /// en: 'Manual Connection'
String get cameraPermissionRequired => 'Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.'; String get manualConnection => 'Manual Connection';
/// en: 'Could not start camera: ${error}' /// en: 'Could not initialize secure connection. Make sure you are signed in to a Plex account.'
String cameraError({required Object error}) => 'Could not start camera: ${error}'; String get cryptoInitFailed => 'Could not initialize secure connection. Make sure you are signed in to a Plex account.';
/// en: 'Point your camera at the QR code shown on your desktop'
String get scanInstruction => 'Point your camera at the QR code shown on your desktop';
/// en: 'Invalid QR code format'
String get invalidQrCode => 'Invalid QR code format';
/// en: 'Please enter host address' /// en: 'Please enter host address'
String get validationHostRequired => 'Please enter host address'; String get validationHostRequired => 'Please enter host address';
@@ -3000,23 +2979,14 @@ class TranslationsCompanionRemotePairingEn {
/// en: 'Format must be IP:port (e.g., 192.168.1.100:48632)' /// en: 'Format must be IP:port (e.g., 192.168.1.100:48632)'
String get validationHostFormat => 'Format must be IP:port (e.g., 192.168.1.100:48632)'; String get validationHostFormat => 'Format must be IP:port (e.g., 192.168.1.100:48632)';
/// en: 'Please enter a session ID' /// en: 'Connection timed out. Make sure both devices are on the same network.'
String get validationSessionIdRequired => 'Please enter a session ID'; String get connectionTimedOut => 'Connection timed out. Make sure both devices are on the same network.';
/// en: 'Session ID must be 8 characters' /// en: 'Could not find the device. Make sure Plezy is running on the host.'
String get validationSessionIdLength => 'Session ID must be 8 characters'; String get sessionNotFound => 'Could not find the device. Make sure Plezy is running on the host.';
/// en: 'Please enter a PIN' /// en: 'Authentication failed. Make sure both devices are on the same Plex account.'
String get validationPinRequired => 'Please enter a PIN'; String get authFailed => 'Authentication failed. Make sure both devices are on the same Plex account.';
/// en: 'PIN must be 6 digits'
String get validationPinLength => 'PIN must be 6 digits';
/// en: 'Connection timed out. Please check the session ID and PIN.'
String get connectionTimedOut => 'Connection timed out. Please check the session ID and PIN.';
/// en: 'Could not find the session. Please check your credentials.'
String get sessionNotFound => 'Could not find the session. Please check your credentials.';
/// en: 'Failed to connect: ${error}' /// en: 'Failed to connect: ${error}'
String failedToConnect({required Object error}) => 'Failed to connect: ${error}'; String failedToConnect({required Object error}) => 'Failed to connect: ${error}';
@@ -3325,6 +3295,8 @@ extension on Translations {
'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%',
'settings.discordRichPresence' => 'Discord Rich Presence', 'settings.discordRichPresence' => 'Discord Rich Presence',
'settings.discordRichPresenceDescription' => 'Show what you\'re watching on Discord', 'settings.discordRichPresenceDescription' => 'Show what you\'re watching on Discord',
'settings.companionRemoteServer' => 'Companion Remote Server',
'settings.companionRemoteServerDescription' => 'Allow mobile devices on your network to control this app',
'settings.autoPip' => 'Auto Picture-in-Picture', 'settings.autoPip' => 'Auto Picture-in-Picture',
'settings.autoPipDescription' => 'Automatically enter picture-in-picture when leaving the app during playback', 'settings.autoPipDescription' => 'Automatically enter picture-in-picture when leaving the app during playback',
'settings.matchContentFrameRate' => 'Match Content Frame Rate', 'settings.matchContentFrameRate' => 'Match Content Frame Rate',
@@ -3636,10 +3608,10 @@ extension on Translations {
'libraries.groupings.seasons' => 'Seasons', 'libraries.groupings.seasons' => 'Seasons',
'libraries.groupings.episodes' => 'Episodes', 'libraries.groupings.episodes' => 'Episodes',
'libraries.groupings.folders' => 'Folders', 'libraries.groupings.folders' => 'Folders',
'about.title' => 'About',
'about.openSourceLicenses' => 'Open Source Licenses',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'About',
'about.openSourceLicenses' => 'Open Source Licenses',
'about.versionLabel' => ({required Object version}) => 'Version ${version}', 'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'A beautiful Plex client for Flutter', 'about.appDescription' => 'A beautiful Plex client for Flutter',
'about.viewLicensesDescription' => 'View licenses of third-party libraries', 'about.viewLicensesDescription' => 'View licenses of third-party libraries',
@@ -3831,45 +3803,33 @@ extension on Translations {
'companionRemote.remoteControl' => 'Remote Control', 'companionRemote.remoteControl' => 'Remote Control',
'companionRemote.controlDesktop' => 'Control a desktop device', 'companionRemote.controlDesktop' => 'Control a desktop device',
'companionRemote.connectedTo' => ({required Object name}) => 'Connected to ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Connected to ${name}',
'companionRemote.session.creatingSession' => 'Creating remote session...', 'companionRemote.session.startingServer' => 'Starting remote server...',
'companionRemote.session.failedToCreate' => 'Failed to create remote session:', 'companionRemote.session.failedToCreate' => 'Failed to start remote server:',
'companionRemote.session.noSession' => 'No session available',
'companionRemote.session.scanQrCode' => 'Scan QR Code',
'companionRemote.session.orEnterManually' => 'Or enter manually',
'companionRemote.session.hostAddress' => 'Host Address', 'companionRemote.session.hostAddress' => 'Host Address',
'companionRemote.session.sessionId' => 'Session ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Connected', 'companionRemote.session.connected' => 'Connected',
'companionRemote.session.waitingForConnection' => 'Waiting for connection...', 'companionRemote.session.serverRunning' => 'Remote server active',
'companionRemote.session.serverStopped' => 'Remote server stopped',
'companionRemote.session.serverRunningDescription' => 'Mobile devices on your network can discover and connect to this app',
'companionRemote.session.serverStoppedDescription' => 'Start the server to allow mobile devices to connect',
'companionRemote.session.usePhoneToControl' => 'Use your mobile device to control this app', 'companionRemote.session.usePhoneToControl' => 'Use your mobile device to control this app',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} copied to clipboard', 'companionRemote.session.startServer' => 'Start Server',
'companionRemote.session.copyToClipboard' => 'Copy to clipboard', 'companionRemote.session.stopServer' => 'Stop Server',
'companionRemote.session.newSession' => 'New Session',
'companionRemote.session.minimize' => 'Minimize', 'companionRemote.session.minimize' => 'Minimize',
'companionRemote.pairing.scan' => 'Scan', 'companionRemote.pairing.pairWithDesktop' => 'Connect to Desktop',
'companionRemote.pairing.manual' => 'Manual', 'companionRemote.pairing.discoveryDescription' => 'Devices on your network running Plezy with the same Plex account will appear automatically',
'companionRemote.pairing.pairWithDesktop' => 'Pair with Desktop',
'companionRemote.pairing.enterSessionDetails' => 'Enter the session details shown on your desktop device',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Enter 8-character session ID',
'companionRemote.pairing.pinHint' => 'Enter 6-digit PIN',
'companionRemote.pairing.connecting' => 'Connecting...', 'companionRemote.pairing.connecting' => 'Connecting...',
'companionRemote.pairing.tips' => 'Tips', 'companionRemote.pairing.searchingForDevices' => 'Looking for devices...',
'companionRemote.pairing.tipDesktop' => 'Open Plezy on your desktop and enable Companion Remote from settings or menu', 'companionRemote.pairing.noDevicesFound' => 'No devices found on your network',
'companionRemote.pairing.tipScan' => 'Use the Scan tab to quickly pair by scanning the QR code on your desktop', 'companionRemote.pairing.noDevicesHint' => 'Make sure Plezy is open on your desktop and both devices are on the same WiFi network',
'companionRemote.pairing.tipWifi' => 'Make sure both devices are on the same WiFi network', 'companionRemote.pairing.availableDevices' => 'Available Devices',
'companionRemote.pairing.cameraPermissionRequired' => 'Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.', 'companionRemote.pairing.manualConnection' => 'Manual Connection',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Could not start camera: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Could not initialize secure connection. Make sure you are signed in to a Plex account.',
'companionRemote.pairing.scanInstruction' => 'Point your camera at the QR code shown on your desktop',
'companionRemote.pairing.invalidQrCode' => 'Invalid QR code format',
'companionRemote.pairing.validationHostRequired' => 'Please enter host address', 'companionRemote.pairing.validationHostRequired' => 'Please enter host address',
'companionRemote.pairing.validationHostFormat' => 'Format must be IP:port (e.g., 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Format must be IP:port (e.g., 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Please enter a session ID', 'companionRemote.pairing.connectionTimedOut' => 'Connection timed out. Make sure both devices are on the same network.',
'companionRemote.pairing.validationSessionIdLength' => 'Session ID must be 8 characters', 'companionRemote.pairing.sessionNotFound' => 'Could not find the device. Make sure Plezy is running on the host.',
'companionRemote.pairing.validationPinRequired' => 'Please enter a PIN', 'companionRemote.pairing.authFailed' => 'Authentication failed. Make sure both devices are on the same Plex account.',
'companionRemote.pairing.validationPinLength' => 'PIN must be 6 digits',
'companionRemote.pairing.connectionTimedOut' => 'Connection timed out. Please check the session ID and PIN.',
'companionRemote.pairing.sessionNotFound' => 'Could not find the session. Please check your credentials.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Failed to connect: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Failed to connect: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Do you want to disconnect from the remote session?', 'companionRemote.remote.disconnectConfirm' => 'Do you want to disconnect from the remote session?',
'companionRemote.remote.reconnecting' => 'Reconnecting...', 'companionRemote.remote.reconnecting' => 'Reconnecting...',
+50 -70
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsEs implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Servidor de control remoto';
@override String get companionRemoteServerDescription => 'Permitir que dispositivos móviles en tu red controlen esta aplicación';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionEs implements TranslationsCompanionRemo
final TranslationsEs _root; // ignore: unused_field final TranslationsEs _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Creando sesión remota...'; @override String get startingServer => 'Iniciando servidor remoto...';
@override String get failedToCreate => 'Error al crear la sesión remota:'; @override String get failedToCreate => 'Error al iniciar el servidor remoto:';
@override String get noSession => 'No hay sesión disponible';
@override String get scanQrCode => 'Escanear código QR';
@override String get orEnterManually => 'O introducir manualmente';
@override String get hostAddress => 'Dirección del host'; @override String get hostAddress => 'Dirección del host';
@override String get sessionId => 'ID de sesión';
@override String get pin => 'PIN';
@override String get connected => 'Conectado'; @override String get connected => 'Conectado';
@override String get waitingForConnection => 'Esperando conexión...'; @override String get serverRunning => 'Servidor remoto activo';
@override String get usePhoneToControl => 'Usa tu dispositivo móvil para controlar esta app'; @override String get serverStopped => 'Servidor remoto detenido';
@override String copiedToClipboard({required Object label}) => '${label} copiado al portapapeles'; @override String get serverRunningDescription => 'Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación';
@override String get copyToClipboard => 'Copiar al portapapeles'; @override String get serverStoppedDescription => 'Inicia el servidor para permitir que los dispositivos móviles se conecten';
@override String get newSession => 'Nueva sesión'; @override String get usePhoneToControl => 'Usa tu dispositivo móvil para controlar esta aplicación';
@override String get startServer => 'Iniciar servidor';
@override String get stopServer => 'Detener servidor';
@override String get minimize => 'Minimizar'; @override String get minimize => 'Minimizar';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingEs implements TranslationsCompanionRemo
final TranslationsEs _root; // ignore: unused_field final TranslationsEs _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Escanear'; @override String get pairWithDesktop => 'Conectar al escritorio';
@override String get manual => 'Manual'; @override String get discoveryDescription => 'Los dispositivos en tu red que ejecutan Plezy con la misma cuenta de Plex aparecerán automáticamente';
@override String get pairWithDesktop => 'Emparejar con escritorio';
@override String get enterSessionDetails => 'Introduce los datos de la sesión que aparecen en tu dispositivo de escritorio';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Introduce ID de sesión de 8 caracteres';
@override String get pinHint => 'Introduce PIN de 6 dígitos';
@override String get connecting => 'Conectando...'; @override String get connecting => 'Conectando...';
@override String get tips => 'Consejos'; @override String get searchingForDevices => 'Buscando dispositivos...';
@override String get tipDesktop => 'Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú'; @override String get noDevicesFound => 'No se encontraron dispositivos en tu red';
@override String get tipScan => 'Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio'; @override String get noDevicesHint => 'Asegúrate de que Plezy esté abierto en tu escritorio y que ambos dispositivos estén en la misma red WiFi';
@override String get tipWifi => 'Asegúrate de que ambos dispositivos estén en la misma red WiFi'; @override String get availableDevices => 'Dispositivos disponibles';
@override String get cameraPermissionRequired => 'Se necesita permiso de cámara para escanear códigos QR.\nPor favor, concede acceso a la cámara en los ajustes de tu dispositivo.'; @override String get manualConnection => 'Conexión manual';
@override String cameraError({required Object error}) => 'No se pudo iniciar la cámara: ${error}'; @override String get cryptoInitFailed => 'No se pudo inicializar la conexión segura. Asegúrate de haber iniciado sesión en una cuenta de Plex.';
@override String get scanInstruction => 'Apunta tu cámara al código QR que aparece en tu escritorio'; @override String get validationHostRequired => 'Ingresa la dirección del host';
@override String get invalidQrCode => 'Formato de código QR no válido'; @override String get validationHostFormat => 'El formato debe ser IP:puerto (ej. 192.168.1.100:48632)';
@override String get validationHostRequired => 'Por favor, introduce la dirección del host'; @override String get connectionTimedOut => 'Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.';
@override String get validationHostFormat => 'El formato debe ser IP:puerto (ej., 192.168.1.100:48632)'; @override String get sessionNotFound => 'No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.';
@override String get validationSessionIdRequired => 'Por favor, introduce un ID de sesión'; @override String get authFailed => 'Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.';
@override String get validationSessionIdLength => 'El ID de sesión debe tener 8 caracteres';
@override String get validationPinRequired => 'Por favor, introduce un PIN';
@override String get validationPinLength => 'El PIN debe tener 6 dígitos';
@override String get connectionTimedOut => 'Tiempo de conexión agotado. Verifica el ID de sesión y el PIN.';
@override String get sessionNotFound => 'No se encontró la sesión. Verifica tus credenciales.';
@override String failedToConnect({required Object error}) => 'Error al conectar: ${error}'; @override String failedToConnect({required Object error}) => 'Error al conectar: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsEs {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Servidor de control remoto',
'settings.companionRemoteServerDescription' => 'Permitir que dispositivos móviles en tu red controlen esta aplicación',
'search.hint' => 'Buscar películas, series, música...', 'search.hint' => 'Buscar películas, series, música...',
'search.tryDifferentTerm' => 'Prueba con un término de búsqueda diferente', 'search.tryDifferentTerm' => 'Prueba con un término de búsqueda diferente',
'search.searchYourMedia' => 'Busca en tu contenido', 'search.searchYourMedia' => 'Busca en tu contenido',
@@ -1881,10 +1873,10 @@ extension on TranslationsEs {
'libraries.groupings.seasons' => 'Temporadas', 'libraries.groupings.seasons' => 'Temporadas',
'libraries.groupings.episodes' => 'Episodios', 'libraries.groupings.episodes' => 'Episodios',
'libraries.groupings.folders' => 'Carpetas', 'libraries.groupings.folders' => 'Carpetas',
'about.title' => 'Acerca de',
'about.openSourceLicenses' => 'Licencias de Código Abierto',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Acerca de',
'about.openSourceLicenses' => 'Licencias de Código Abierto',
'about.versionLabel' => ({required Object version}) => 'Versión ${version}', 'about.versionLabel' => ({required Object version}) => 'Versión ${version}',
'about.appDescription' => 'Un cliente de Plex para Flutter', 'about.appDescription' => 'Un cliente de Plex para Flutter',
'about.viewLicensesDescription' => 'Ver licencias de librerías de terceros', 'about.viewLicensesDescription' => 'Ver licencias de librerías de terceros',
@@ -2076,45 +2068,33 @@ extension on TranslationsEs {
'companionRemote.remoteControl' => 'Control remoto', 'companionRemote.remoteControl' => 'Control remoto',
'companionRemote.controlDesktop' => 'Controlar un dispositivo de escritorio', 'companionRemote.controlDesktop' => 'Controlar un dispositivo de escritorio',
'companionRemote.connectedTo' => ({required Object name}) => 'Conectado a ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Conectado a ${name}',
'companionRemote.session.creatingSession' => 'Creando sesión remota...', 'companionRemote.session.startingServer' => 'Iniciando servidor remoto...',
'companionRemote.session.failedToCreate' => 'Error al crear la sesión remota:', 'companionRemote.session.failedToCreate' => 'Error al iniciar el servidor remoto:',
'companionRemote.session.noSession' => 'No hay sesión disponible',
'companionRemote.session.scanQrCode' => 'Escanear código QR',
'companionRemote.session.orEnterManually' => 'O introducir manualmente',
'companionRemote.session.hostAddress' => 'Dirección del host', 'companionRemote.session.hostAddress' => 'Dirección del host',
'companionRemote.session.sessionId' => 'ID de sesión',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Conectado', 'companionRemote.session.connected' => 'Conectado',
'companionRemote.session.waitingForConnection' => 'Esperando conexión...', 'companionRemote.session.serverRunning' => 'Servidor remoto activo',
'companionRemote.session.usePhoneToControl' => 'Usa tu dispositivo móvil para controlar esta app', 'companionRemote.session.serverStopped' => 'Servidor remoto detenido',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} copiado al portapapeles', 'companionRemote.session.serverRunningDescription' => 'Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación',
'companionRemote.session.copyToClipboard' => 'Copiar al portapapeles', 'companionRemote.session.serverStoppedDescription' => 'Inicia el servidor para permitir que los dispositivos móviles se conecten',
'companionRemote.session.newSession' => 'Nueva sesión', 'companionRemote.session.usePhoneToControl' => 'Usa tu dispositivo móvil para controlar esta aplicación',
'companionRemote.session.startServer' => 'Iniciar servidor',
'companionRemote.session.stopServer' => 'Detener servidor',
'companionRemote.session.minimize' => 'Minimizar', 'companionRemote.session.minimize' => 'Minimizar',
'companionRemote.pairing.scan' => 'Escanear', 'companionRemote.pairing.pairWithDesktop' => 'Conectar al escritorio',
'companionRemote.pairing.manual' => 'Manual', 'companionRemote.pairing.discoveryDescription' => 'Los dispositivos en tu red que ejecutan Plezy con la misma cuenta de Plex aparecerán automáticamente',
'companionRemote.pairing.pairWithDesktop' => 'Emparejar con escritorio',
'companionRemote.pairing.enterSessionDetails' => 'Introduce los datos de la sesión que aparecen en tu dispositivo de escritorio',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Introduce ID de sesión de 8 caracteres',
'companionRemote.pairing.pinHint' => 'Introduce PIN de 6 dígitos',
'companionRemote.pairing.connecting' => 'Conectando...', 'companionRemote.pairing.connecting' => 'Conectando...',
'companionRemote.pairing.tips' => 'Consejos', 'companionRemote.pairing.searchingForDevices' => 'Buscando dispositivos...',
'companionRemote.pairing.tipDesktop' => 'Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú', 'companionRemote.pairing.noDevicesFound' => 'No se encontraron dispositivos en tu red',
'companionRemote.pairing.tipScan' => 'Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio', 'companionRemote.pairing.noDevicesHint' => 'Asegúrate de que Plezy esté abierto en tu escritorio y que ambos dispositivos estén en la misma red WiFi',
'companionRemote.pairing.tipWifi' => 'Asegúrate de que ambos dispositivos estén en la misma red WiFi', 'companionRemote.pairing.availableDevices' => 'Dispositivos disponibles',
'companionRemote.pairing.cameraPermissionRequired' => 'Se necesita permiso de cámara para escanear códigos QR.\nPor favor, concede acceso a la cámara en los ajustes de tu dispositivo.', 'companionRemote.pairing.manualConnection' => 'Conexión manual',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'No se pudo iniciar la cámara: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'No se pudo inicializar la conexión segura. Asegúrate de haber iniciado sesión en una cuenta de Plex.',
'companionRemote.pairing.scanInstruction' => 'Apunta tu cámara al código QR que aparece en tu escritorio', 'companionRemote.pairing.validationHostRequired' => 'Ingresa la dirección del host',
'companionRemote.pairing.invalidQrCode' => 'Formato de código QR no válido', 'companionRemote.pairing.validationHostFormat' => 'El formato debe ser IP:puerto (ej. 192.168.1.100:48632)',
'companionRemote.pairing.validationHostRequired' => 'Por favor, introduce la dirección del host', 'companionRemote.pairing.connectionTimedOut' => 'Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.',
'companionRemote.pairing.validationHostFormat' => 'El formato debe ser IP:puerto (ej., 192.168.1.100:48632)', 'companionRemote.pairing.sessionNotFound' => 'No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.',
'companionRemote.pairing.validationSessionIdRequired' => 'Por favor, introduce un ID de sesión', 'companionRemote.pairing.authFailed' => 'Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.',
'companionRemote.pairing.validationSessionIdLength' => 'El ID de sesión debe tener 8 caracteres',
'companionRemote.pairing.validationPinRequired' => 'Por favor, introduce un PIN',
'companionRemote.pairing.validationPinLength' => 'El PIN debe tener 6 dígitos',
'companionRemote.pairing.connectionTimedOut' => 'Tiempo de conexión agotado. Verifica el ID de sesión y el PIN.',
'companionRemote.pairing.sessionNotFound' => 'No se encontró la sesión. Verifica tus credenciales.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Error al conectar: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Error al conectar: ${error}',
'companionRemote.remote.disconnectConfirm' => '¿Quieres desconectarte de la sesión remota?', 'companionRemote.remote.disconnectConfirm' => '¿Quieres desconectarte de la sesión remota?',
'companionRemote.remote.reconnecting' => 'Reconectando...', 'companionRemote.remote.reconnecting' => 'Reconectando...',
+48 -68
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsFr implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Serveur de télécommande';
@override String get companionRemoteServerDescription => 'Autoriser les appareils mobiles de votre réseau à contrôler cette application';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionFr implements TranslationsCompanionRemo
final TranslationsFr _root; // ignore: unused_field final TranslationsFr _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Création de la session distante...'; @override String get startingServer => 'Démarrage du serveur distant...';
@override String get failedToCreate => 'Échec de la création de la session distante :'; @override String get failedToCreate => 'Échec du démarrage du serveur distant :';
@override String get noSession => 'Aucune session disponible';
@override String get scanQrCode => 'Scanner le QR Code';
@override String get orEnterManually => 'Ou saisir manuellement';
@override String get hostAddress => 'Adresse de l\'hôte'; @override String get hostAddress => 'Adresse de l\'hôte';
@override String get sessionId => 'ID de session';
@override String get pin => 'PIN';
@override String get connected => 'Connecté'; @override String get connected => 'Connecté';
@override String get waitingForConnection => 'En attente de connexion...'; @override String get serverRunning => 'Serveur distant actif';
@override String get serverStopped => 'Serveur distant arrêté';
@override String get serverRunningDescription => 'Les appareils mobiles de votre réseau peuvent découvrir et se connecter à cette application';
@override String get serverStoppedDescription => 'Démarrez le serveur pour permettre aux appareils mobiles de se connecter';
@override String get usePhoneToControl => 'Utilisez votre appareil mobile pour contrôler cette application'; @override String get usePhoneToControl => 'Utilisez votre appareil mobile pour contrôler cette application';
@override String copiedToClipboard({required Object label}) => '${label} copié dans le presse-papiers'; @override String get startServer => 'Démarrer le serveur';
@override String get copyToClipboard => 'Copier dans le presse-papiers'; @override String get stopServer => 'Arrêter le serveur';
@override String get newSession => 'Nouvelle session';
@override String get minimize => 'Réduire'; @override String get minimize => 'Réduire';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingFr implements TranslationsCompanionRemo
final TranslationsFr _root; // ignore: unused_field final TranslationsFr _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Scanner'; @override String get pairWithDesktop => 'Se connecter au bureau';
@override String get manual => 'Manuel'; @override String get discoveryDescription => 'Les appareils de votre réseau exécutant Plezy avec le même compte Plex apparaîtront automatiquement';
@override String get pairWithDesktop => 'Jumeler avec un bureau';
@override String get enterSessionDetails => 'Saisissez les détails de la session affichés sur votre appareil de bureau';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Saisissez l\'ID de session à 8 caractères';
@override String get pinHint => 'Saisissez le PIN à 6 chiffres';
@override String get connecting => 'Connexion...'; @override String get connecting => 'Connexion...';
@override String get tips => 'Conseils'; @override String get searchingForDevices => 'Recherche d\'appareils...';
@override String get tipDesktop => 'Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu'; @override String get noDevicesFound => 'Aucun appareil trouvé sur votre réseau';
@override String get tipScan => 'Utilisez l\'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau'; @override String get noDevicesHint => 'Assurez-vous que Plezy est ouvert sur votre bureau et que les deux appareils sont sur le même réseau WiFi';
@override String get tipWifi => 'Assurez-vous que les deux appareils sont sur le même réseau WiFi'; @override String get availableDevices => 'Appareils disponibles';
@override String get cameraPermissionRequired => 'L\'autorisation de la caméra est requise pour scanner les QR codes.\nVeuillez accorder l\'accès à la caméra dans les paramètres de votre appareil.'; @override String get manualConnection => 'Connexion manuelle';
@override String cameraError({required Object error}) => 'Impossible de démarrer la caméra : ${error}'; @override String get cryptoInitFailed => 'Impossible d\'initialiser la connexion sécurisée. Assurez-vous d\'être connecté à un compte Plex.';
@override String get scanInstruction => 'Pointez votre caméra vers le QR code affiché sur votre bureau'; @override String get validationHostRequired => 'Veuillez entrer l\'adresse de l\'hôte';
@override String get invalidQrCode => 'Format de QR code invalide'; @override String get validationHostFormat => 'Le format doit être IP:port (ex. 192.168.1.100:48632)';
@override String get validationHostRequired => 'Veuillez saisir l\'adresse de l\'hôte'; @override String get connectionTimedOut => 'Délai de connexion dépassé. Assurez-vous que les deux appareils sont sur le même réseau.';
@override String get validationHostFormat => 'Le format doit être IP:port (ex : 192.168.1.100:48632)'; @override String get sessionNotFound => 'Appareil introuvable. Assurez-vous que Plezy est en cours d\'exécution sur l\'hôte.';
@override String get validationSessionIdRequired => 'Veuillez saisir un ID de session'; @override String get authFailed => 'Échec de l\'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.';
@override String get validationSessionIdLength => 'L\'ID de session doit contenir 8 caractères';
@override String get validationPinRequired => 'Veuillez saisir un PIN';
@override String get validationPinLength => 'Le PIN doit contenir 6 chiffres';
@override String get connectionTimedOut => 'Délai de connexion expiré. Veuillez vérifier l\'ID de session et le PIN.';
@override String get sessionNotFound => 'Session introuvable. Veuillez vérifier vos identifiants.';
@override String failedToConnect({required Object error}) => 'Échec de la connexion : ${error}'; @override String failedToConnect({required Object error}) => 'Échec de la connexion : ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsFr {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Serveur de télécommande',
'settings.companionRemoteServerDescription' => 'Autoriser les appareils mobiles de votre réseau à contrôler cette application',
'search.hint' => 'Rechercher des films, des séries, de la musique...', 'search.hint' => 'Rechercher des films, des séries, de la musique...',
'search.tryDifferentTerm' => 'Essayez un autre terme de recherche', 'search.tryDifferentTerm' => 'Essayez un autre terme de recherche',
'search.searchYourMedia' => 'Rechercher dans vos médias', 'search.searchYourMedia' => 'Rechercher dans vos médias',
@@ -1881,10 +1873,10 @@ extension on TranslationsFr {
'libraries.groupings.seasons' => 'Saisons', 'libraries.groupings.seasons' => 'Saisons',
'libraries.groupings.episodes' => 'Épisodes', 'libraries.groupings.episodes' => 'Épisodes',
'libraries.groupings.folders' => 'Dossiers', 'libraries.groupings.folders' => 'Dossiers',
'about.title' => 'À propos',
'about.openSourceLicenses' => 'Licences Open Source',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'À propos',
'about.openSourceLicenses' => 'Licences Open Source',
'about.versionLabel' => ({required Object version}) => 'Version ${version}', 'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'Un magnifique client Plex pour Flutter', 'about.appDescription' => 'Un magnifique client Plex pour Flutter',
'about.viewLicensesDescription' => 'Afficher les licences des bibliothèques tierces', 'about.viewLicensesDescription' => 'Afficher les licences des bibliothèques tierces',
@@ -2076,45 +2068,33 @@ extension on TranslationsFr {
'companionRemote.remoteControl' => 'Télécommande', 'companionRemote.remoteControl' => 'Télécommande',
'companionRemote.controlDesktop' => 'Contrôler un appareil de bureau', 'companionRemote.controlDesktop' => 'Contrôler un appareil de bureau',
'companionRemote.connectedTo' => ({required Object name}) => 'Connecté à ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Connecté à ${name}',
'companionRemote.session.creatingSession' => 'Création de la session distante...', 'companionRemote.session.startingServer' => 'Démarrage du serveur distant...',
'companionRemote.session.failedToCreate' => 'Échec de la création de la session distante :', 'companionRemote.session.failedToCreate' => 'Échec du démarrage du serveur distant :',
'companionRemote.session.noSession' => 'Aucune session disponible',
'companionRemote.session.scanQrCode' => 'Scanner le QR Code',
'companionRemote.session.orEnterManually' => 'Ou saisir manuellement',
'companionRemote.session.hostAddress' => 'Adresse de l\'hôte', 'companionRemote.session.hostAddress' => 'Adresse de l\'hôte',
'companionRemote.session.sessionId' => 'ID de session',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Connecté', 'companionRemote.session.connected' => 'Connecté',
'companionRemote.session.waitingForConnection' => 'En attente de connexion...', 'companionRemote.session.serverRunning' => 'Serveur distant actif',
'companionRemote.session.serverStopped' => 'Serveur distant arrêté',
'companionRemote.session.serverRunningDescription' => 'Les appareils mobiles de votre réseau peuvent découvrir et se connecter à cette application',
'companionRemote.session.serverStoppedDescription' => 'Démarrez le serveur pour permettre aux appareils mobiles de se connecter',
'companionRemote.session.usePhoneToControl' => 'Utilisez votre appareil mobile pour contrôler cette application', 'companionRemote.session.usePhoneToControl' => 'Utilisez votre appareil mobile pour contrôler cette application',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} copié dans le presse-papiers', 'companionRemote.session.startServer' => 'Démarrer le serveur',
'companionRemote.session.copyToClipboard' => 'Copier dans le presse-papiers', 'companionRemote.session.stopServer' => 'Arrêter le serveur',
'companionRemote.session.newSession' => 'Nouvelle session',
'companionRemote.session.minimize' => 'Réduire', 'companionRemote.session.minimize' => 'Réduire',
'companionRemote.pairing.scan' => 'Scanner', 'companionRemote.pairing.pairWithDesktop' => 'Se connecter au bureau',
'companionRemote.pairing.manual' => 'Manuel', 'companionRemote.pairing.discoveryDescription' => 'Les appareils de votre réseau exécutant Plezy avec le même compte Plex apparaîtront automatiquement',
'companionRemote.pairing.pairWithDesktop' => 'Jumeler avec un bureau',
'companionRemote.pairing.enterSessionDetails' => 'Saisissez les détails de la session affichés sur votre appareil de bureau',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Saisissez l\'ID de session à 8 caractères',
'companionRemote.pairing.pinHint' => 'Saisissez le PIN à 6 chiffres',
'companionRemote.pairing.connecting' => 'Connexion...', 'companionRemote.pairing.connecting' => 'Connexion...',
'companionRemote.pairing.tips' => 'Conseils', 'companionRemote.pairing.searchingForDevices' => 'Recherche d\'appareils...',
'companionRemote.pairing.tipDesktop' => 'Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu', 'companionRemote.pairing.noDevicesFound' => 'Aucun appareil trouvé sur votre réseau',
'companionRemote.pairing.tipScan' => 'Utilisez l\'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau', 'companionRemote.pairing.noDevicesHint' => 'Assurez-vous que Plezy est ouvert sur votre bureau et que les deux appareils sont sur le même réseau WiFi',
'companionRemote.pairing.tipWifi' => 'Assurez-vous que les deux appareils sont sur le même réseau WiFi', 'companionRemote.pairing.availableDevices' => 'Appareils disponibles',
'companionRemote.pairing.cameraPermissionRequired' => 'L\'autorisation de la caméra est requise pour scanner les QR codes.\nVeuillez accorder l\'accès à la caméra dans les paramètres de votre appareil.', 'companionRemote.pairing.manualConnection' => 'Connexion manuelle',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Impossible de démarrer la caméra : ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Impossible d\'initialiser la connexion sécurisée. Assurez-vous d\'être connecté à un compte Plex.',
'companionRemote.pairing.scanInstruction' => 'Pointez votre caméra vers le QR code affiché sur votre bureau', 'companionRemote.pairing.validationHostRequired' => 'Veuillez entrer l\'adresse de l\'hôte',
'companionRemote.pairing.invalidQrCode' => 'Format de QR code invalide', 'companionRemote.pairing.validationHostFormat' => 'Le format doit être IP:port (ex. 192.168.1.100:48632)',
'companionRemote.pairing.validationHostRequired' => 'Veuillez saisir l\'adresse de l\'hôte', 'companionRemote.pairing.connectionTimedOut' => 'Délai de connexion dépassé. Assurez-vous que les deux appareils sont sur le même réseau.',
'companionRemote.pairing.validationHostFormat' => 'Le format doit être IP:port (ex : 192.168.1.100:48632)', 'companionRemote.pairing.sessionNotFound' => 'Appareil introuvable. Assurez-vous que Plezy est en cours d\'exécution sur l\'hôte.',
'companionRemote.pairing.validationSessionIdRequired' => 'Veuillez saisir un ID de session', 'companionRemote.pairing.authFailed' => 'Échec de l\'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.',
'companionRemote.pairing.validationSessionIdLength' => 'L\'ID de session doit contenir 8 caractères',
'companionRemote.pairing.validationPinRequired' => 'Veuillez saisir un PIN',
'companionRemote.pairing.validationPinLength' => 'Le PIN doit contenir 6 chiffres',
'companionRemote.pairing.connectionTimedOut' => 'Délai de connexion expiré. Veuillez vérifier l\'ID de session et le PIN.',
'companionRemote.pairing.sessionNotFound' => 'Session introuvable. Veuillez vérifier vos identifiants.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Échec de la connexion : ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Échec de la connexion : ${error}',
'companionRemote.remote.disconnectConfirm' => 'Voulez-vous vous déconnecter de la session distante ?', 'companionRemote.remote.disconnectConfirm' => 'Voulez-vous vous déconnecter de la session distante ?',
'companionRemote.remote.reconnecting' => 'Reconnexion...', 'companionRemote.remote.reconnecting' => 'Reconnexion...',
+44 -64
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Server telecomando';
@override String get companionRemoteServerDescription => 'Consenti ai dispositivi mobili nella tua rete di controllare questa app';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionIt implements TranslationsCompanionRemo
final TranslationsIt _root; // ignore: unused_field final TranslationsIt _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Creazione sessione remota...'; @override String get startingServer => 'Avvio del server remoto...';
@override String get failedToCreate => 'Impossibile creare la sessione remota:'; @override String get failedToCreate => 'Impossibile avviare il server remoto:';
@override String get noSession => 'Nessuna sessione disponibile';
@override String get scanQrCode => 'Scansiona QR Code';
@override String get orEnterManually => 'Oppure inserisci manualmente';
@override String get hostAddress => 'Indirizzo host'; @override String get hostAddress => 'Indirizzo host';
@override String get sessionId => 'ID sessione';
@override String get pin => 'PIN';
@override String get connected => 'Connesso'; @override String get connected => 'Connesso';
@override String get waitingForConnection => 'In attesa di connessione...'; @override String get serverRunning => 'Server remoto attivo';
@override String get serverStopped => 'Server remoto arrestato';
@override String get serverRunningDescription => 'I dispositivi mobili nella tua rete possono scoprire e connettersi a questa app';
@override String get serverStoppedDescription => 'Avvia il server per consentire ai dispositivi mobili di connettersi';
@override String get usePhoneToControl => 'Usa il tuo dispositivo mobile per controllare questa app'; @override String get usePhoneToControl => 'Usa il tuo dispositivo mobile per controllare questa app';
@override String copiedToClipboard({required Object label}) => '${label} copiato negli appunti'; @override String get startServer => 'Avvia server';
@override String get copyToClipboard => 'Copia negli appunti'; @override String get stopServer => 'Arresta server';
@override String get newSession => 'Nuova sessione';
@override String get minimize => 'Riduci'; @override String get minimize => 'Riduci';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingIt implements TranslationsCompanionRemo
final TranslationsIt _root; // ignore: unused_field final TranslationsIt _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Scansiona'; @override String get pairWithDesktop => 'Connetti al desktop';
@override String get manual => 'Manuale'; @override String get discoveryDescription => 'I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente';
@override String get pairWithDesktop => 'Associa con desktop';
@override String get enterSessionDetails => 'Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Inserisci ID sessione di 8 caratteri';
@override String get pinHint => 'Inserisci PIN di 6 cifre';
@override String get connecting => 'Connessione...'; @override String get connecting => 'Connessione...';
@override String get tips => 'Suggerimenti'; @override String get searchingForDevices => 'Ricerca dispositivi...';
@override String get tipDesktop => 'Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu'; @override String get noDevicesFound => 'Nessun dispositivo trovato nella tua rete';
@override String get tipScan => 'Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop'; @override String get noDevicesHint => 'Assicurati che Plezy sia aperto sul tuo desktop e che entrambi i dispositivi siano sulla stessa rete WiFi';
@override String get tipWifi => 'Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi'; @override String get availableDevices => 'Dispositivi disponibili';
@override String get cameraPermissionRequired => 'L\'autorizzazione della fotocamera è necessaria per scansionare i QR code.\nConcedi l\'accesso alla fotocamera nelle impostazioni del dispositivo.'; @override String get manualConnection => 'Connessione manuale';
@override String cameraError({required Object error}) => 'Impossibile avviare la fotocamera: ${error}'; @override String get cryptoInitFailed => 'Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l\'accesso a un account Plex.';
@override String get scanInstruction => 'Punta la fotocamera verso il QR code mostrato sul tuo desktop';
@override String get invalidQrCode => 'Formato QR code non valido';
@override String get validationHostRequired => 'Inserisci l\'indirizzo host'; @override String get validationHostRequired => 'Inserisci l\'indirizzo host';
@override String get validationHostFormat => 'Il formato deve essere IP:porta (es. 192.168.1.100:48632)'; @override String get validationHostFormat => 'Il formato deve essere IP:porta (es. 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Inserisci un ID sessione'; @override String get connectionTimedOut => 'Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.';
@override String get validationSessionIdLength => 'L\'ID sessione deve essere di 8 caratteri'; @override String get sessionNotFound => 'Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull\'host.';
@override String get validationPinRequired => 'Inserisci un PIN'; @override String get authFailed => 'Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.';
@override String get validationPinLength => 'Il PIN deve essere di 6 cifre';
@override String get connectionTimedOut => 'Connessione scaduta. Verifica l\'ID sessione e il PIN.';
@override String get sessionNotFound => 'Sessione non trovata. Verifica le tue credenziali.';
@override String failedToConnect({required Object error}) => 'Connessione fallita: ${error}'; @override String failedToConnect({required Object error}) => 'Connessione fallita: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsIt {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Server telecomando',
'settings.companionRemoteServerDescription' => 'Consenti ai dispositivi mobili nella tua rete di controllare questa app',
'search.hint' => 'Cerca film. spettacoli, musica...', 'search.hint' => 'Cerca film. spettacoli, musica...',
'search.tryDifferentTerm' => 'Prova altri termini di ricerca', 'search.tryDifferentTerm' => 'Prova altri termini di ricerca',
'search.searchYourMedia' => 'Cerca nei tuoi media', 'search.searchYourMedia' => 'Cerca nei tuoi media',
@@ -1881,10 +1873,10 @@ extension on TranslationsIt {
'libraries.groupings.seasons' => 'Stagioni', 'libraries.groupings.seasons' => 'Stagioni',
'libraries.groupings.episodes' => 'Episodi', 'libraries.groupings.episodes' => 'Episodi',
'libraries.groupings.folders' => 'Cartelle', 'libraries.groupings.folders' => 'Cartelle',
'about.title' => 'Informazioni',
'about.openSourceLicenses' => 'Licenze Open Source',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Informazioni',
'about.openSourceLicenses' => 'Licenze Open Source',
'about.versionLabel' => ({required Object version}) => 'Versione ${version}', 'about.versionLabel' => ({required Object version}) => 'Versione ${version}',
'about.appDescription' => 'Un bellissimo client Plex per Flutter', 'about.appDescription' => 'Un bellissimo client Plex per Flutter',
'about.viewLicensesDescription' => 'Visualizza le licenze delle librerie di terze parti', 'about.viewLicensesDescription' => 'Visualizza le licenze delle librerie di terze parti',
@@ -2076,45 +2068,33 @@ extension on TranslationsIt {
'companionRemote.remoteControl' => 'Telecomando', 'companionRemote.remoteControl' => 'Telecomando',
'companionRemote.controlDesktop' => 'Controlla un dispositivo desktop', 'companionRemote.controlDesktop' => 'Controlla un dispositivo desktop',
'companionRemote.connectedTo' => ({required Object name}) => 'Connesso a ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Connesso a ${name}',
'companionRemote.session.creatingSession' => 'Creazione sessione remota...', 'companionRemote.session.startingServer' => 'Avvio del server remoto...',
'companionRemote.session.failedToCreate' => 'Impossibile creare la sessione remota:', 'companionRemote.session.failedToCreate' => 'Impossibile avviare il server remoto:',
'companionRemote.session.noSession' => 'Nessuna sessione disponibile',
'companionRemote.session.scanQrCode' => 'Scansiona QR Code',
'companionRemote.session.orEnterManually' => 'Oppure inserisci manualmente',
'companionRemote.session.hostAddress' => 'Indirizzo host', 'companionRemote.session.hostAddress' => 'Indirizzo host',
'companionRemote.session.sessionId' => 'ID sessione',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Connesso', 'companionRemote.session.connected' => 'Connesso',
'companionRemote.session.waitingForConnection' => 'In attesa di connessione...', 'companionRemote.session.serverRunning' => 'Server remoto attivo',
'companionRemote.session.serverStopped' => 'Server remoto arrestato',
'companionRemote.session.serverRunningDescription' => 'I dispositivi mobili nella tua rete possono scoprire e connettersi a questa app',
'companionRemote.session.serverStoppedDescription' => 'Avvia il server per consentire ai dispositivi mobili di connettersi',
'companionRemote.session.usePhoneToControl' => 'Usa il tuo dispositivo mobile per controllare questa app', 'companionRemote.session.usePhoneToControl' => 'Usa il tuo dispositivo mobile per controllare questa app',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} copiato negli appunti', 'companionRemote.session.startServer' => 'Avvia server',
'companionRemote.session.copyToClipboard' => 'Copia negli appunti', 'companionRemote.session.stopServer' => 'Arresta server',
'companionRemote.session.newSession' => 'Nuova sessione',
'companionRemote.session.minimize' => 'Riduci', 'companionRemote.session.minimize' => 'Riduci',
'companionRemote.pairing.scan' => 'Scansiona', 'companionRemote.pairing.pairWithDesktop' => 'Connetti al desktop',
'companionRemote.pairing.manual' => 'Manuale', 'companionRemote.pairing.discoveryDescription' => 'I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente',
'companionRemote.pairing.pairWithDesktop' => 'Associa con desktop',
'companionRemote.pairing.enterSessionDetails' => 'Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Inserisci ID sessione di 8 caratteri',
'companionRemote.pairing.pinHint' => 'Inserisci PIN di 6 cifre',
'companionRemote.pairing.connecting' => 'Connessione...', 'companionRemote.pairing.connecting' => 'Connessione...',
'companionRemote.pairing.tips' => 'Suggerimenti', 'companionRemote.pairing.searchingForDevices' => 'Ricerca dispositivi...',
'companionRemote.pairing.tipDesktop' => 'Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu', 'companionRemote.pairing.noDevicesFound' => 'Nessun dispositivo trovato nella tua rete',
'companionRemote.pairing.tipScan' => 'Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop', 'companionRemote.pairing.noDevicesHint' => 'Assicurati che Plezy sia aperto sul tuo desktop e che entrambi i dispositivi siano sulla stessa rete WiFi',
'companionRemote.pairing.tipWifi' => 'Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi', 'companionRemote.pairing.availableDevices' => 'Dispositivi disponibili',
'companionRemote.pairing.cameraPermissionRequired' => 'L\'autorizzazione della fotocamera è necessaria per scansionare i QR code.\nConcedi l\'accesso alla fotocamera nelle impostazioni del dispositivo.', 'companionRemote.pairing.manualConnection' => 'Connessione manuale',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Impossibile avviare la fotocamera: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l\'accesso a un account Plex.',
'companionRemote.pairing.scanInstruction' => 'Punta la fotocamera verso il QR code mostrato sul tuo desktop',
'companionRemote.pairing.invalidQrCode' => 'Formato QR code non valido',
'companionRemote.pairing.validationHostRequired' => 'Inserisci l\'indirizzo host', 'companionRemote.pairing.validationHostRequired' => 'Inserisci l\'indirizzo host',
'companionRemote.pairing.validationHostFormat' => 'Il formato deve essere IP:porta (es. 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Il formato deve essere IP:porta (es. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Inserisci un ID sessione', 'companionRemote.pairing.connectionTimedOut' => 'Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.',
'companionRemote.pairing.validationSessionIdLength' => 'L\'ID sessione deve essere di 8 caratteri', 'companionRemote.pairing.sessionNotFound' => 'Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull\'host.',
'companionRemote.pairing.validationPinRequired' => 'Inserisci un PIN', 'companionRemote.pairing.authFailed' => 'Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.',
'companionRemote.pairing.validationPinLength' => 'Il PIN deve essere di 6 cifre',
'companionRemote.pairing.connectionTimedOut' => 'Connessione scaduta. Verifica l\'ID sessione e il PIN.',
'companionRemote.pairing.sessionNotFound' => 'Sessione non trovata. Verifica le tue credenziali.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Connessione fallita: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Connessione fallita: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vuoi disconnetterti dalla sessione remota?', 'companionRemote.remote.disconnectConfirm' => 'Vuoi disconnetterti dalla sessione remota?',
'companionRemote.remote.reconnecting' => 'Riconnessione...', 'companionRemote.remote.reconnecting' => 'Riconnessione...',
+46 -66
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsJa implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'コンパニオンリモートサーバー';
@override String get companionRemoteServerDescription => 'ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionJa implements TranslationsCompanionRemo
final TranslationsJa _root; // ignore: unused_field final TranslationsJa _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'リモートセッションを作成中...'; @override String get startingServer => 'リモートサーバーを起動中...';
@override String get failedToCreate => 'リモートセッションの作成に失敗しました:'; @override String get failedToCreate => 'リモートサーバーの起動に失敗しました:';
@override String get noSession => '利用可能なセッションがありません';
@override String get scanQrCode => 'QRコードをスキャン';
@override String get orEnterManually => 'または手動で入力';
@override String get hostAddress => 'ホストアドレス'; @override String get hostAddress => 'ホストアドレス';
@override String get sessionId => 'セッションID';
@override String get pin => 'PIN';
@override String get connected => '接続済み'; @override String get connected => '接続済み';
@override String get waitingForConnection => '接続を待機中...'; @override String get serverRunning => 'リモートサーバー稼働中';
@override String get usePhoneToControl => 'モバイルデバイスでこのアプリを操作'; @override String get serverStopped => 'リモートサーバー停止中';
@override String copiedToClipboard({required Object label}) => '${label}をクリップボードにコピーしました'; @override String get serverRunningDescription => 'ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます';
@override String get copyToClipboard => 'クリップボードにコピー'; @override String get serverStoppedDescription => 'モバイルデバイスの接続を許可するにはサーバーを起動してください';
@override String get newSession => '新しいセッション'; @override String get usePhoneToControl => 'モバイルデバイスでこのアプリを操作できます';
@override String get startServer => 'サーバーを起動';
@override String get stopServer => 'サーバーを停止';
@override String get minimize => '最小化'; @override String get minimize => '最小化';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingJa implements TranslationsCompanionRemo
final TranslationsJa _root; // ignore: unused_field final TranslationsJa _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'スキャン'; @override String get pairWithDesktop => 'デスクトップに接続';
@override String get manual => '手動'; @override String get discoveryDescription => '同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます';
@override String get pairWithDesktop => 'デスクトップとペアリング';
@override String get enterSessionDetails => 'デスクトップデバイスに表示されたセッション情報を入力';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => '8文字のセッションIDを入力';
@override String get pinHint => '6桁のPINを入力';
@override String get connecting => '接続中...'; @override String get connecting => '接続中...';
@override String get tips => 'ヒント'; @override String get searchingForDevices => 'デバイスを検索中...';
@override String get tipDesktop => 'デスクトップでPlezyを開き、設定またはメニューからコンパニオンリモートを有効にしてください'; @override String get noDevicesFound => 'ネットワーク上にデバイスが見つかりません';
@override String get tipScan => 'スキャンタブを使用して、デスクトップのQRコードをスキャンして素早くペアリング'; @override String get noDevicesHint => 'デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください';
@override String get tipWifi => '両方のデバイスが同じWiFiネットワークに接続されていることを確認'; @override String get availableDevices => '利用可能なデバイス';
@override String get cameraPermissionRequired => 'QRコードをスキャンするにはカメラの権限が必要です。\nデバイスの設定でカメラへのアクセスを許可してください。'; @override String get manualConnection => '手動接続';
@override String cameraError({required Object error}) => 'カメラを起動できませんでした: ${error}'; @override String get cryptoInitFailed => '安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。';
@override String get scanInstruction => 'デスクトップに表示されたQRコードにカメラを向けてください';
@override String get invalidQrCode => '無効なQRコード形式';
@override String get validationHostRequired => 'ホストアドレスを入力してください'; @override String get validationHostRequired => 'ホストアドレスを入力してください';
@override String get validationHostFormat => '形式はIP:ポートである必要があります(例: 192.168.1.100:48632'; @override String get validationHostFormat => '形式はIP:ポートである必要があります(例: 192.168.1.100:48632';
@override String get validationSessionIdRequired => 'セッションIDを入力してください'; @override String get connectionTimedOut => '接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください';
@override String get validationSessionIdLength => 'セッションIDは8文字である必要があります'; @override String get sessionNotFound => 'デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。';
@override String get validationPinRequired => 'PINを入力してください'; @override String get authFailed => '認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください';
@override String get validationPinLength => 'PINは6桁である必要があります';
@override String get connectionTimedOut => '接続がタイムアウトしました。セッションIDとPINを確認してください。';
@override String get sessionNotFound => 'セッションが見つかりませんでした。認証情報を確認してください。';
@override String failedToConnect({required Object error}) => '接続に失敗しました: ${error}'; @override String failedToConnect({required Object error}) => '接続に失敗しました: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsJa {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'コンパニオンリモートサーバー',
'settings.companionRemoteServerDescription' => 'ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする',
'search.hint' => '映画、番組、音楽を検索...', 'search.hint' => '映画、番組、音楽を検索...',
'search.tryDifferentTerm' => '別の検索語をお試しください', 'search.tryDifferentTerm' => '別の検索語をお試しください',
'search.searchYourMedia' => 'メディアを検索', 'search.searchYourMedia' => 'メディアを検索',
@@ -1881,10 +1873,10 @@ extension on TranslationsJa {
'libraries.groupings.seasons' => 'シーズン', 'libraries.groupings.seasons' => 'シーズン',
'libraries.groupings.episodes' => 'エピソード', 'libraries.groupings.episodes' => 'エピソード',
'libraries.groupings.folders' => 'フォルダ', 'libraries.groupings.folders' => 'フォルダ',
'about.title' => 'アプリについて',
'about.openSourceLicenses' => 'オープンソースライセンス',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'アプリについて',
'about.openSourceLicenses' => 'オープンソースライセンス',
'about.versionLabel' => ({required Object version}) => 'バージョン ${version}', 'about.versionLabel' => ({required Object version}) => 'バージョン ${version}',
'about.appDescription' => 'Flutter製の美しいPlexクライアント', 'about.appDescription' => 'Flutter製の美しいPlexクライアント',
'about.viewLicensesDescription' => 'サードパーティライブラリのライセンスを表示', 'about.viewLicensesDescription' => 'サードパーティライブラリのライセンスを表示',
@@ -2076,45 +2068,33 @@ extension on TranslationsJa {
'companionRemote.remoteControl' => 'リモコン', 'companionRemote.remoteControl' => 'リモコン',
'companionRemote.controlDesktop' => 'デスクトップデバイスを操作', 'companionRemote.controlDesktop' => 'デスクトップデバイスを操作',
'companionRemote.connectedTo' => ({required Object name}) => '${name}に接続中', 'companionRemote.connectedTo' => ({required Object name}) => '${name}に接続中',
'companionRemote.session.creatingSession' => 'リモートセッションを作成中...', 'companionRemote.session.startingServer' => 'リモートサーバーを起動中...',
'companionRemote.session.failedToCreate' => 'リモートセッションの作成に失敗しました:', 'companionRemote.session.failedToCreate' => 'リモートサーバーの起動に失敗しました:',
'companionRemote.session.noSession' => '利用可能なセッションがありません',
'companionRemote.session.scanQrCode' => 'QRコードをスキャン',
'companionRemote.session.orEnterManually' => 'または手動で入力',
'companionRemote.session.hostAddress' => 'ホストアドレス', 'companionRemote.session.hostAddress' => 'ホストアドレス',
'companionRemote.session.sessionId' => 'セッションID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => '接続済み', 'companionRemote.session.connected' => '接続済み',
'companionRemote.session.waitingForConnection' => '接続を待機中...', 'companionRemote.session.serverRunning' => 'リモートサーバー稼働中',
'companionRemote.session.usePhoneToControl' => 'モバイルデバイスでこのアプリを操作', 'companionRemote.session.serverStopped' => 'リモートサーバー停止中',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label}をクリップボードにコピーしました', 'companionRemote.session.serverRunningDescription' => 'ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます',
'companionRemote.session.copyToClipboard' => 'クリップボードにコピー', 'companionRemote.session.serverStoppedDescription' => 'モバイルデバイスの接続を許可するにはサーバーを起動してください',
'companionRemote.session.newSession' => '新しいセッション', 'companionRemote.session.usePhoneToControl' => 'モバイルデバイスでこのアプリを操作できます',
'companionRemote.session.startServer' => 'サーバーを起動',
'companionRemote.session.stopServer' => 'サーバーを停止',
'companionRemote.session.minimize' => '最小化', 'companionRemote.session.minimize' => '最小化',
'companionRemote.pairing.scan' => 'スキャン', 'companionRemote.pairing.pairWithDesktop' => 'デスクトップに接続',
'companionRemote.pairing.manual' => '手動', 'companionRemote.pairing.discoveryDescription' => '同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます',
'companionRemote.pairing.pairWithDesktop' => 'デスクトップとペアリング',
'companionRemote.pairing.enterSessionDetails' => 'デスクトップデバイスに表示されたセッション情報を入力',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '8文字のセッションIDを入力',
'companionRemote.pairing.pinHint' => '6桁のPINを入力',
'companionRemote.pairing.connecting' => '接続中...', 'companionRemote.pairing.connecting' => '接続中...',
'companionRemote.pairing.tips' => 'ヒント', 'companionRemote.pairing.searchingForDevices' => 'デバイスを検索中...',
'companionRemote.pairing.tipDesktop' => 'デスクトップでPlezyを開き、設定またはメニューからコンパニオンリモートを有効にしてください', 'companionRemote.pairing.noDevicesFound' => 'ネットワーク上にデバイスが見つかりません',
'companionRemote.pairing.tipScan' => 'スキャンタブを使用して、デスクトップのQRコードをスキャンして素早くペアリング', 'companionRemote.pairing.noDevicesHint' => 'デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください',
'companionRemote.pairing.tipWifi' => '両方のデバイスが同じWiFiネットワークに接続されていることを確認', 'companionRemote.pairing.availableDevices' => '利用可能なデバイス',
'companionRemote.pairing.cameraPermissionRequired' => 'QRコードをスキャンするにはカメラの権限が必要です。\nデバイスの設定でカメラへのアクセスを許可してください。', 'companionRemote.pairing.manualConnection' => '手動接続',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'カメラを起動できませんでした: ${error}', 'companionRemote.pairing.cryptoInitFailed' => '安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。',
'companionRemote.pairing.scanInstruction' => 'デスクトップに表示されたQRコードにカメラを向けてください',
'companionRemote.pairing.invalidQrCode' => '無効なQRコード形式',
'companionRemote.pairing.validationHostRequired' => 'ホストアドレスを入力してください', 'companionRemote.pairing.validationHostRequired' => 'ホストアドレスを入力してください',
'companionRemote.pairing.validationHostFormat' => '形式はIP:ポートである必要があります(例: 192.168.1.100:48632', 'companionRemote.pairing.validationHostFormat' => '形式はIP:ポートである必要があります(例: 192.168.1.100:48632',
'companionRemote.pairing.validationSessionIdRequired' => 'セッションIDを入力してください', 'companionRemote.pairing.connectionTimedOut' => '接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください',
'companionRemote.pairing.validationSessionIdLength' => 'セッションIDは8文字である必要があります', 'companionRemote.pairing.sessionNotFound' => 'デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。',
'companionRemote.pairing.validationPinRequired' => 'PINを入力してください', 'companionRemote.pairing.authFailed' => '認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください',
'companionRemote.pairing.validationPinLength' => 'PINは6桁である必要があります',
'companionRemote.pairing.connectionTimedOut' => '接続がタイムアウトしました。セッションIDとPINを確認してください。',
'companionRemote.pairing.sessionNotFound' => 'セッションが見つかりませんでした。認証情報を確認してください。',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => '接続に失敗しました: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => '接続に失敗しました: ${error}',
'companionRemote.remote.disconnectConfirm' => 'リモートセッションから切断しますか?', 'companionRemote.remote.disconnectConfirm' => 'リモートセッションから切断しますか?',
'companionRemote.remote.reconnecting' => '再接続中...', 'companionRemote.remote.reconnecting' => '再接続中...',
+46 -66
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => '컴패니언 리모트 서버';
@override String get companionRemoteServerDescription => '네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionKo implements TranslationsCompanionRemo
final TranslationsKo _root; // ignore: unused_field final TranslationsKo _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => '원격 세션 생성 중...'; @override String get startingServer => '원격 서버 시작 중...';
@override String get failedToCreate => '원격 세션 생성 실패:'; @override String get failedToCreate => '원격 서버를 시작하지 못했습니다:';
@override String get noSession => '사용 가능한 세션이 없습니다';
@override String get scanQrCode => 'QR 코드 스캔';
@override String get orEnterManually => '또는 수동으로 입력';
@override String get hostAddress => '호스트 주소'; @override String get hostAddress => '호스트 주소';
@override String get sessionId => '세션 ID';
@override String get pin => 'PIN';
@override String get connected => '연결됨'; @override String get connected => '연결됨';
@override String get waitingForConnection => '연결 대기 중...'; @override String get serverRunning => '원격 서버 활성';
@override String get serverStopped => '원격 서버 중지됨';
@override String get serverRunningDescription => '네트워크의 모바일 기기가 이 앱을 검색하고 연결할 수 있습니다';
@override String get serverStoppedDescription => '모바일 기기의 연결을 허용하려면 서버를 시작하세요';
@override String get usePhoneToControl => '모바일 기기로 이 앱을 제어하세요'; @override String get usePhoneToControl => '모바일 기기로 이 앱을 제어하세요';
@override String copiedToClipboard({required Object label}) => '${label}이(가) 클립보드에 복사되었습니다'; @override String get startServer => '서버 시작';
@override String get copyToClipboard => '클립보드에 복사'; @override String get stopServer => '서버 중지';
@override String get newSession => '새 세션';
@override String get minimize => '최소화'; @override String get minimize => '최소화';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingKo implements TranslationsCompanionRemo
final TranslationsKo _root; // ignore: unused_field final TranslationsKo _root; // ignore: unused_field
// Translations // Translations
@override String get scan => '스캔'; @override String get pairWithDesktop => '데스크톱에 연결';
@override String get manual => '수동'; @override String get discoveryDescription => '같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다';
@override String get pairWithDesktop => '데스크톱과 페어링';
@override String get enterSessionDetails => '데스크톱 기기에 표시된 세션 정보를 입력하세요';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => '8자리 세션 ID 입력';
@override String get pinHint => '6자리 PIN 입력';
@override String get connecting => '연결 중...'; @override String get connecting => '연결 중...';
@override String get tips => ''; @override String get searchingForDevices => '기기 검색 중...';
@override String get tipDesktop => '데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요'; @override String get noDevicesFound => '네트워크에서 기기를 찾을 수 없습니다';
@override String get tipScan => '스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다'; @override String get noDevicesHint => '데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요';
@override String get tipWifi => '두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요'; @override String get availableDevices => '사용 가능한 기기';
@override String get cameraPermissionRequired => 'QR 코드를 스캔하려면 카메라 권한이 필요합니다.\n기기 설정에서 카메라 접근을 허용해 주세요.'; @override String get manualConnection => '수동 연결';
@override String cameraError({required Object error}) => '카메라를 시작할 수 없습니다: ${error}'; @override String get cryptoInitFailed => '보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.';
@override String get scanInstruction => '데스크톱에 표시된 QR 코드에 카메라를 향하세요';
@override String get invalidQrCode => '유효하지 않은 QR 코드 형식';
@override String get validationHostRequired => '호스트 주소를 입력하세요'; @override String get validationHostRequired => '호스트 주소를 입력하세요';
@override String get validationHostFormat => 'IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)'; @override String get validationHostFormat => '형식은 IP:포트야 합니다 (예: 192.168.1.100:48632)';
@override String get validationSessionIdRequired => '세션 ID를 입력하세요'; @override String get connectionTimedOut => '연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.';
@override String get validationSessionIdLength => '세션 ID는 8자리여야 합니다'; @override String get sessionNotFound => '기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.';
@override String get validationPinRequired => 'PIN을 입력하세요'; @override String get authFailed => '인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.';
@override String get validationPinLength => 'PIN은 6자리여야 합니다';
@override String get connectionTimedOut => '연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.';
@override String get sessionNotFound => '세션을 찾을 수 없습니다. 자격 증명을 확인하세요.';
@override String failedToConnect({required Object error}) => '연결 실패: ${error}'; @override String failedToConnect({required Object error}) => '연결 실패: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsKo {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => '컴패니언 리모트 서버',
'settings.companionRemoteServerDescription' => '네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용',
'search.hint' => '영화, 시리즈, 음악 등을 검색하세요...', 'search.hint' => '영화, 시리즈, 음악 등을 검색하세요...',
'search.tryDifferentTerm' => '다른 검색어를 시도해 보세요', 'search.tryDifferentTerm' => '다른 검색어를 시도해 보세요',
'search.searchYourMedia' => '미디어 검색', 'search.searchYourMedia' => '미디어 검색',
@@ -1881,10 +1873,10 @@ extension on TranslationsKo {
'libraries.groupings.seasons' => '시즌', 'libraries.groupings.seasons' => '시즌',
'libraries.groupings.episodes' => '', 'libraries.groupings.episodes' => '',
'libraries.groupings.folders' => '폴더', 'libraries.groupings.folders' => '폴더',
'about.title' => '소개',
'about.openSourceLicenses' => '오픈소스 라이선스',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => '소개',
'about.openSourceLicenses' => '오픈소스 라이선스',
'about.versionLabel' => ({required Object version}) => '버전 ${version}', 'about.versionLabel' => ({required Object version}) => '버전 ${version}',
'about.appDescription' => '아름다운 Flutter Plex 클라이언트', 'about.appDescription' => '아름다운 Flutter Plex 클라이언트',
'about.viewLicensesDescription' => '타사 라이브러리 라이선스 보기', 'about.viewLicensesDescription' => '타사 라이브러리 라이선스 보기',
@@ -2076,45 +2068,33 @@ extension on TranslationsKo {
'companionRemote.remoteControl' => '원격 제어', 'companionRemote.remoteControl' => '원격 제어',
'companionRemote.controlDesktop' => '데스크톱 기기 제어', 'companionRemote.controlDesktop' => '데스크톱 기기 제어',
'companionRemote.connectedTo' => ({required Object name}) => '${name}에 연결됨', 'companionRemote.connectedTo' => ({required Object name}) => '${name}에 연결됨',
'companionRemote.session.creatingSession' => '원격 세션 생성 중...', 'companionRemote.session.startingServer' => '원격 서버 시작 중...',
'companionRemote.session.failedToCreate' => '원격 세션 생성 실패:', 'companionRemote.session.failedToCreate' => '원격 서버를 시작하지 못했습니다:',
'companionRemote.session.noSession' => '사용 가능한 세션이 없습니다',
'companionRemote.session.scanQrCode' => 'QR 코드 스캔',
'companionRemote.session.orEnterManually' => '또는 수동으로 입력',
'companionRemote.session.hostAddress' => '호스트 주소', 'companionRemote.session.hostAddress' => '호스트 주소',
'companionRemote.session.sessionId' => '세션 ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => '연결됨', 'companionRemote.session.connected' => '연결됨',
'companionRemote.session.waitingForConnection' => '연결 대기 중...', 'companionRemote.session.serverRunning' => '원격 서버 활성',
'companionRemote.session.serverStopped' => '원격 서버 중지됨',
'companionRemote.session.serverRunningDescription' => '네트워크의 모바일 기기가 이 앱을 검색하고 연결할 수 있습니다',
'companionRemote.session.serverStoppedDescription' => '모바일 기기의 연결을 허용하려면 서버를 시작하세요',
'companionRemote.session.usePhoneToControl' => '모바일 기기로 이 앱을 제어하세요', 'companionRemote.session.usePhoneToControl' => '모바일 기기로 이 앱을 제어하세요',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label}이(가) 클립보드에 복사되었습니다', 'companionRemote.session.startServer' => '서버 시작',
'companionRemote.session.copyToClipboard' => '클립보드에 복사', 'companionRemote.session.stopServer' => '서버 중지',
'companionRemote.session.newSession' => '새 세션',
'companionRemote.session.minimize' => '최소화', 'companionRemote.session.minimize' => '최소화',
'companionRemote.pairing.scan' => '스캔', 'companionRemote.pairing.pairWithDesktop' => '데스크톱에 연결',
'companionRemote.pairing.manual' => '수동', 'companionRemote.pairing.discoveryDescription' => '같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다',
'companionRemote.pairing.pairWithDesktop' => '데스크톱과 페어링',
'companionRemote.pairing.enterSessionDetails' => '데스크톱 기기에 표시된 세션 정보를 입력하세요',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '8자리 세션 ID 입력',
'companionRemote.pairing.pinHint' => '6자리 PIN 입력',
'companionRemote.pairing.connecting' => '연결 중...', 'companionRemote.pairing.connecting' => '연결 중...',
'companionRemote.pairing.tips' => '', 'companionRemote.pairing.searchingForDevices' => '기기 검색 중...',
'companionRemote.pairing.tipDesktop' => '데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요', 'companionRemote.pairing.noDevicesFound' => '네트워크에서 기기를 찾을 수 없습니다',
'companionRemote.pairing.tipScan' => '스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다', 'companionRemote.pairing.noDevicesHint' => '데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요',
'companionRemote.pairing.tipWifi' => '두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요', 'companionRemote.pairing.availableDevices' => '사용 가능한 기기',
'companionRemote.pairing.cameraPermissionRequired' => 'QR 코드를 스캔하려면 카메라 권한이 필요합니다.\n기기 설정에서 카메라 접근을 허용해 주세요.', 'companionRemote.pairing.manualConnection' => '수동 연결',
'companionRemote.pairing.cameraError' => ({required Object error}) => '카메라를 시작할 수 없습니다: ${error}', 'companionRemote.pairing.cryptoInitFailed' => '보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.',
'companionRemote.pairing.scanInstruction' => '데스크톱에 표시된 QR 코드에 카메라를 향하세요',
'companionRemote.pairing.invalidQrCode' => '유효하지 않은 QR 코드 형식',
'companionRemote.pairing.validationHostRequired' => '호스트 주소를 입력하세요', 'companionRemote.pairing.validationHostRequired' => '호스트 주소를 입력하세요',
'companionRemote.pairing.validationHostFormat' => 'IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => '형식은 IP:포트야 합니다 (예: 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => '세션 ID를 입력하세요', 'companionRemote.pairing.connectionTimedOut' => '연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.',
'companionRemote.pairing.validationSessionIdLength' => '세션 ID는 8자리여야 합니다', 'companionRemote.pairing.sessionNotFound' => '기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.',
'companionRemote.pairing.validationPinRequired' => 'PIN을 입력하세요', 'companionRemote.pairing.authFailed' => '인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.',
'companionRemote.pairing.validationPinLength' => 'PIN은 6자리여야 합니다',
'companionRemote.pairing.connectionTimedOut' => '연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.',
'companionRemote.pairing.sessionNotFound' => '세션을 찾을 수 없습니다. 자격 증명을 확인하세요.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => '연결 실패: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => '연결 실패: ${error}',
'companionRemote.remote.disconnectConfirm' => '원격 세션 연결을 해제하시겠습니까?', 'companionRemote.remote.disconnectConfirm' => '원격 세션 연결을 해제하시겠습니까?',
'companionRemote.remote.reconnecting' => '재연결 중...', 'companionRemote.remote.reconnecting' => '재연결 중...',
+50 -70
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsNb implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Companion Remote-server';
@override String get companionRemoteServerDescription => 'Tillat mobilenheter på nettverket ditt å styre denne appen';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionNb implements TranslationsCompanionRemo
final TranslationsNb _root; // ignore: unused_field final TranslationsNb _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Oppretter fjernøkt...'; @override String get startingServer => 'Starter fjernserver...';
@override String get failedToCreate => 'Kunne ikke opprette fjernøkt:'; @override String get failedToCreate => 'Kunne ikke starte fjernserver:';
@override String get noSession => 'Ingen økt tilgjengelig';
@override String get scanQrCode => 'Skann QR-kode';
@override String get orEnterManually => 'Eller skriv inn manuelt';
@override String get hostAddress => 'Vertsadresse'; @override String get hostAddress => 'Vertsadresse';
@override String get sessionId => 'Økt-ID';
@override String get pin => 'PIN';
@override String get connected => 'Tilkoblet'; @override String get connected => 'Tilkoblet';
@override String get waitingForConnection => 'Venter på tilkobling...'; @override String get serverRunning => 'Fjernserver aktiv';
@override String get usePhoneToControl => 'Bruk mobilenheten din til å kontrollere denne appen'; @override String get serverStopped => 'Fjernserver stoppet';
@override String copiedToClipboard({required Object label}) => '${label} kopiert til utklippstavle'; @override String get serverRunningDescription => 'Mobilenheter på nettverket ditt kan oppdage og koble til denne appen';
@override String get copyToClipboard => 'Kopier til utklippstavle'; @override String get serverStoppedDescription => 'Start serveren for å la mobilenheter koble til';
@override String get newSession => 'Ny økt'; @override String get usePhoneToControl => 'Bruk mobilenheten din til å styre denne appen';
@override String get startServer => 'Start server';
@override String get stopServer => 'Stopp server';
@override String get minimize => 'Minimer'; @override String get minimize => 'Minimer';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingNb implements TranslationsCompanionRemo
final TranslationsNb _root; // ignore: unused_field final TranslationsNb _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Skann'; @override String get pairWithDesktop => 'Koble til datamaskin';
@override String get manual => 'Manuell'; @override String get discoveryDescription => 'Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk';
@override String get pairWithDesktop => 'Koble til stasjonær';
@override String get enterSessionDetails => 'Skriv inn øktdetaljene som vises på den stasjonære enheten din';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Skriv inn 8-tegns økt-ID';
@override String get pinHint => 'Skriv inn 6-sifret PIN';
@override String get connecting => 'Kobler til...'; @override String get connecting => 'Kobler til...';
@override String get tips => 'Tips'; @override String get searchingForDevices => 'Søker etter enheter...';
@override String get tipDesktop => 'Åpne Plezy på datamaskinen din og aktiver Følgesvenn-fjernkontroll fra innstillinger eller meny'; @override String get noDevicesFound => 'Ingen enheter funnet på nettverket ditt';
@override String get tipScan => 'Bruk Skann-fanen for å raskt koble til ved å skanne QR-koden på datamaskinen din'; @override String get noDevicesHint => 'Sørg for at Plezy er åpen på datamaskinen din og at begge enhetene er på samme WiFi-nettverk';
@override String get tipWifi => 'Sørg for at begge enhetene er på samme WiFi-nettverk'; @override String get availableDevices => 'Tilgjengelige enheter';
@override String get cameraPermissionRequired => 'Kameratillatelse kreves for å skanne QR-koder.\nVennligst gi kameratilgang i enhetsinnstillingene.'; @override String get manualConnection => 'Manuell tilkobling';
@override String cameraError({required Object error}) => 'Kunne ikke starte kamera: ${error}'; @override String get cryptoInitFailed => 'Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.';
@override String get scanInstruction => 'Pek kameraet mot QR-koden som vises på datamaskinen din'; @override String get validationHostRequired => 'Vennligst oppgi vertsadresse';
@override String get invalidQrCode => 'Ugyldig QR-kodeformat'; @override String get validationHostFormat => 'Format må være IP:port (f.eks. 192.168.1.100:48632)';
@override String get validationHostRequired => 'Vennligst skriv inn vertsadresse'; @override String get connectionTimedOut => 'Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.';
@override String get validationHostFormat => 'Formatet må være IP:port (f.eks. 192.168.1.100:48632)'; @override String get sessionNotFound => 'Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.';
@override String get validationSessionIdRequired => 'Vennligst skriv inn en økt-ID'; @override String get authFailed => 'Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.';
@override String get validationSessionIdLength => 'Økt-ID må være 8 tegn';
@override String get validationPinRequired => 'Vennligst skriv inn en PIN';
@override String get validationPinLength => 'PIN må være 6 sifre';
@override String get connectionTimedOut => 'Tilkoblingen ble tidsavbrutt. Sjekk økt-ID og PIN.';
@override String get sessionNotFound => 'Kunne ikke finne økten. Sjekk legitimasjonen din.';
@override String failedToConnect({required Object error}) => 'Kunne ikke koble til: ${error}'; @override String failedToConnect({required Object error}) => 'Kunne ikke koble til: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsNb {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Companion Remote-server',
'settings.companionRemoteServerDescription' => 'Tillat mobilenheter på nettverket ditt å styre denne appen',
'search.hint' => 'Søk i filmer, serier, musikk...', 'search.hint' => 'Søk i filmer, serier, musikk...',
'search.tryDifferentTerm' => 'Prøv et annet søkeord', 'search.tryDifferentTerm' => 'Prøv et annet søkeord',
'search.searchYourMedia' => 'Søk i mediene dine', 'search.searchYourMedia' => 'Søk i mediene dine',
@@ -1881,10 +1873,10 @@ extension on TranslationsNb {
'libraries.groupings.seasons' => 'Sesonger', 'libraries.groupings.seasons' => 'Sesonger',
'libraries.groupings.episodes' => 'Episoder', 'libraries.groupings.episodes' => 'Episoder',
'libraries.groupings.folders' => 'Mapper', 'libraries.groupings.folders' => 'Mapper',
'about.title' => 'Om',
'about.openSourceLicenses' => 'Åpen kildekode-lisenser',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Om',
'about.openSourceLicenses' => 'Åpen kildekode-lisenser',
'about.versionLabel' => ({required Object version}) => 'Versjon ${version}', 'about.versionLabel' => ({required Object version}) => 'Versjon ${version}',
'about.appDescription' => 'En vakker Plex-klient for Flutter', 'about.appDescription' => 'En vakker Plex-klient for Flutter',
'about.viewLicensesDescription' => 'Vis lisenser for tredjepartsbiblioteker', 'about.viewLicensesDescription' => 'Vis lisenser for tredjepartsbiblioteker',
@@ -2076,45 +2068,33 @@ extension on TranslationsNb {
'companionRemote.remoteControl' => 'Fjernkontroll', 'companionRemote.remoteControl' => 'Fjernkontroll',
'companionRemote.controlDesktop' => 'Kontroller en stasjonær enhet', 'companionRemote.controlDesktop' => 'Kontroller en stasjonær enhet',
'companionRemote.connectedTo' => ({required Object name}) => 'Tilkoblet ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Tilkoblet ${name}',
'companionRemote.session.creatingSession' => 'Oppretter fjernøkt...', 'companionRemote.session.startingServer' => 'Starter fjernserver...',
'companionRemote.session.failedToCreate' => 'Kunne ikke opprette fjernøkt:', 'companionRemote.session.failedToCreate' => 'Kunne ikke starte fjernserver:',
'companionRemote.session.noSession' => 'Ingen økt tilgjengelig',
'companionRemote.session.scanQrCode' => 'Skann QR-kode',
'companionRemote.session.orEnterManually' => 'Eller skriv inn manuelt',
'companionRemote.session.hostAddress' => 'Vertsadresse', 'companionRemote.session.hostAddress' => 'Vertsadresse',
'companionRemote.session.sessionId' => 'Økt-ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Tilkoblet', 'companionRemote.session.connected' => 'Tilkoblet',
'companionRemote.session.waitingForConnection' => 'Venter på tilkobling...', 'companionRemote.session.serverRunning' => 'Fjernserver aktiv',
'companionRemote.session.usePhoneToControl' => 'Bruk mobilenheten din til å kontrollere denne appen', 'companionRemote.session.serverStopped' => 'Fjernserver stoppet',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} kopiert til utklippstavle', 'companionRemote.session.serverRunningDescription' => 'Mobilenheter på nettverket ditt kan oppdage og koble til denne appen',
'companionRemote.session.copyToClipboard' => 'Kopier til utklippstavle', 'companionRemote.session.serverStoppedDescription' => 'Start serveren for å la mobilenheter koble til',
'companionRemote.session.newSession' => 'Ny økt', 'companionRemote.session.usePhoneToControl' => 'Bruk mobilenheten din til å styre denne appen',
'companionRemote.session.startServer' => 'Start server',
'companionRemote.session.stopServer' => 'Stopp server',
'companionRemote.session.minimize' => 'Minimer', 'companionRemote.session.minimize' => 'Minimer',
'companionRemote.pairing.scan' => 'Skann', 'companionRemote.pairing.pairWithDesktop' => 'Koble til datamaskin',
'companionRemote.pairing.manual' => 'Manuell', 'companionRemote.pairing.discoveryDescription' => 'Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk',
'companionRemote.pairing.pairWithDesktop' => 'Koble til stasjonær',
'companionRemote.pairing.enterSessionDetails' => 'Skriv inn øktdetaljene som vises på den stasjonære enheten din',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Skriv inn 8-tegns økt-ID',
'companionRemote.pairing.pinHint' => 'Skriv inn 6-sifret PIN',
'companionRemote.pairing.connecting' => 'Kobler til...', 'companionRemote.pairing.connecting' => 'Kobler til...',
'companionRemote.pairing.tips' => 'Tips', 'companionRemote.pairing.searchingForDevices' => 'Søker etter enheter...',
'companionRemote.pairing.tipDesktop' => 'Åpne Plezy på datamaskinen din og aktiver Følgesvenn-fjernkontroll fra innstillinger eller meny', 'companionRemote.pairing.noDevicesFound' => 'Ingen enheter funnet på nettverket ditt',
'companionRemote.pairing.tipScan' => 'Bruk Skann-fanen for å raskt koble til ved å skanne QR-koden på datamaskinen din', 'companionRemote.pairing.noDevicesHint' => 'Sørg for at Plezy er åpen på datamaskinen din og at begge enhetene er på samme WiFi-nettverk',
'companionRemote.pairing.tipWifi' => 'Sørg for at begge enhetene er på samme WiFi-nettverk', 'companionRemote.pairing.availableDevices' => 'Tilgjengelige enheter',
'companionRemote.pairing.cameraPermissionRequired' => 'Kameratillatelse kreves for å skanne QR-koder.\nVennligst gi kameratilgang i enhetsinnstillingene.', 'companionRemote.pairing.manualConnection' => 'Manuell tilkobling',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kunne ikke starte kamera: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.',
'companionRemote.pairing.scanInstruction' => 'Pek kameraet mot QR-koden som vises på datamaskinen din', 'companionRemote.pairing.validationHostRequired' => 'Vennligst oppgi vertsadresse',
'companionRemote.pairing.invalidQrCode' => 'Ugyldig QR-kodeformat', 'companionRemote.pairing.validationHostFormat' => 'Format må være IP:port (f.eks. 192.168.1.100:48632)',
'companionRemote.pairing.validationHostRequired' => 'Vennligst skriv inn vertsadresse', 'companionRemote.pairing.connectionTimedOut' => 'Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.',
'companionRemote.pairing.validationHostFormat' => 'Formatet må være IP:port (f.eks. 192.168.1.100:48632)', 'companionRemote.pairing.sessionNotFound' => 'Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.',
'companionRemote.pairing.validationSessionIdRequired' => 'Vennligst skriv inn en økt-ID', 'companionRemote.pairing.authFailed' => 'Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.',
'companionRemote.pairing.validationSessionIdLength' => 'Økt-ID må være 8 tegn',
'companionRemote.pairing.validationPinRequired' => 'Vennligst skriv inn en PIN',
'companionRemote.pairing.validationPinLength' => 'PIN må være 6 sifre',
'companionRemote.pairing.connectionTimedOut' => 'Tilkoblingen ble tidsavbrutt. Sjekk økt-ID og PIN.',
'companionRemote.pairing.sessionNotFound' => 'Kunne ikke finne økten. Sjekk legitimasjonen din.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke koble til: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke koble til: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vil du koble fra fjernøkten?', 'companionRemote.remote.disconnectConfirm' => 'Vil du koble fra fjernøkten?',
'companionRemote.remote.reconnecting' => 'Kobler til på nytt...', 'companionRemote.remote.reconnecting' => 'Kobler til på nytt...',
+48 -68
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsNl implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Companion Remote-server';
@override String get companionRemoteServerDescription => 'Sta mobiele apparaten op je netwerk toe om deze app te bedienen';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionNl implements TranslationsCompanionRemo
final TranslationsNl _root; // ignore: unused_field final TranslationsNl _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Externe sessie aanmaken...'; @override String get startingServer => 'Externe server starten...';
@override String get failedToCreate => 'Kan externe sessie niet aanmaken:'; @override String get failedToCreate => 'Kan externe server niet starten:';
@override String get noSession => 'Geen sessie beschikbaar';
@override String get scanQrCode => 'Scan QR-code';
@override String get orEnterManually => 'Of voer handmatig in';
@override String get hostAddress => 'Hostadres'; @override String get hostAddress => 'Hostadres';
@override String get sessionId => 'Sessie-ID';
@override String get pin => 'PIN';
@override String get connected => 'Verbonden'; @override String get connected => 'Verbonden';
@override String get waitingForConnection => 'Wachten op verbinding...'; @override String get serverRunning => 'Externe server actief';
@override String get serverStopped => 'Externe server gestopt';
@override String get serverRunningDescription => 'Mobiele apparaten op je netwerk kunnen deze app ontdekken en ermee verbinden';
@override String get serverStoppedDescription => 'Start de server om mobiele apparaten te laten verbinden';
@override String get usePhoneToControl => 'Gebruik je mobiele apparaat om deze app te bedienen'; @override String get usePhoneToControl => 'Gebruik je mobiele apparaat om deze app te bedienen';
@override String copiedToClipboard({required Object label}) => '${label} gekopieerd naar klembord'; @override String get startServer => 'Server starten';
@override String get copyToClipboard => 'Kopieer naar klembord'; @override String get stopServer => 'Server stoppen';
@override String get newSession => 'Nieuwe sessie';
@override String get minimize => 'Minimaliseren'; @override String get minimize => 'Minimaliseren';
} }
@@ -1298,31 +1297,22 @@ class _TranslationsCompanionRemotePairingNl implements TranslationsCompanionRemo
final TranslationsNl _root; // ignore: unused_field final TranslationsNl _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Scannen'; @override String get pairWithDesktop => 'Verbinden met desktop';
@override String get manual => 'Handmatig'; @override String get discoveryDescription => 'Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch';
@override String get pairWithDesktop => 'Koppelen met desktop';
@override String get enterSessionDetails => 'Voer de sessiegegevens in die op je desktop-apparaat worden getoond';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Voer 8-tekens sessie-ID in';
@override String get pinHint => 'Voer 6-cijferige PIN in';
@override String get connecting => 'Verbinden...'; @override String get connecting => 'Verbinden...';
@override String get tips => 'Tips'; @override String get searchingForDevices => 'Apparaten zoeken...';
@override String get tipDesktop => 'Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu'; @override String get noDevicesFound => 'Geen apparaten gevonden op je netwerk';
@override String get tipScan => 'Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen'; @override String get noDevicesHint => 'Zorg ervoor dat Plezy geopend is op je desktop en dat beide apparaten op hetzelfde WiFi-netwerk zitten';
@override String get tipWifi => 'Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten'; @override String get availableDevices => 'Beschikbare apparaten';
@override String get cameraPermissionRequired => 'Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.'; @override String get manualConnection => 'Handmatige verbinding';
@override String cameraError({required Object error}) => 'Kan camera niet starten: ${error}'; @override String get cryptoInitFailed => 'Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.';
@override String get scanInstruction => 'Richt je camera op de QR-code die op je desktop wordt getoond'; @override String get validationHostRequired => 'Voer het hostadres in';
@override String get invalidQrCode => 'Ongeldig QR-codeformaat';
@override String get validationHostRequired => 'Voer een hostadres in';
@override String get validationHostFormat => 'Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)'; @override String get validationHostFormat => 'Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Voer een sessie-ID in'; @override String get connectionTimedOut => 'Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.';
@override String get validationSessionIdLength => 'Sessie-ID moet 8 tekens zijn'; @override String get sessionNotFound => 'Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.';
@override String get validationPinRequired => 'Voer een PIN in'; @override String get authFailed => 'Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.';
@override String get validationPinLength => 'PIN moet 6 cijfers zijn'; @override String failedToConnect({required Object error}) => 'Kan niet verbinden: ${error}';
@override String get connectionTimedOut => 'Verbinding verlopen. Controleer de sessie-ID en PIN.';
@override String get sessionNotFound => 'Kan de sessie niet vinden. Controleer je gegevens.';
@override String failedToConnect({required Object error}) => 'Verbinden mislukt: ${error}';
} }
// Path: companionRemote.remote // Path: companionRemote.remote
@@ -1600,6 +1590,8 @@ extension on TranslationsNl {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Companion Remote-server',
'settings.companionRemoteServerDescription' => 'Sta mobiele apparaten op je netwerk toe om deze app te bedienen',
'search.hint' => 'Zoek films, series, muziek...', 'search.hint' => 'Zoek films, series, muziek...',
'search.tryDifferentTerm' => 'Probeer een andere zoekterm', 'search.tryDifferentTerm' => 'Probeer een andere zoekterm',
'search.searchYourMedia' => 'Zoek in je media', 'search.searchYourMedia' => 'Zoek in je media',
@@ -1881,10 +1873,10 @@ extension on TranslationsNl {
'libraries.groupings.seasons' => 'Seizoenen', 'libraries.groupings.seasons' => 'Seizoenen',
'libraries.groupings.episodes' => 'Afleveringen', 'libraries.groupings.episodes' => 'Afleveringen',
'libraries.groupings.folders' => 'Mappen', 'libraries.groupings.folders' => 'Mappen',
'about.title' => 'Over',
'about.openSourceLicenses' => 'Open Source licenties',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Over',
'about.openSourceLicenses' => 'Open Source licenties',
'about.versionLabel' => ({required Object version}) => 'Versie ${version}', 'about.versionLabel' => ({required Object version}) => 'Versie ${version}',
'about.appDescription' => 'Een mooie Plex client voor Flutter', 'about.appDescription' => 'Een mooie Plex client voor Flutter',
'about.viewLicensesDescription' => 'Bekijk licenties van third-party bibliotheken', 'about.viewLicensesDescription' => 'Bekijk licenties van third-party bibliotheken',
@@ -2076,46 +2068,34 @@ extension on TranslationsNl {
'companionRemote.remoteControl' => 'Afstandsbediening', 'companionRemote.remoteControl' => 'Afstandsbediening',
'companionRemote.controlDesktop' => 'Bedien een desktop-apparaat', 'companionRemote.controlDesktop' => 'Bedien een desktop-apparaat',
'companionRemote.connectedTo' => ({required Object name}) => 'Verbonden met ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Verbonden met ${name}',
'companionRemote.session.creatingSession' => 'Externe sessie aanmaken...', 'companionRemote.session.startingServer' => 'Externe server starten...',
'companionRemote.session.failedToCreate' => 'Kan externe sessie niet aanmaken:', 'companionRemote.session.failedToCreate' => 'Kan externe server niet starten:',
'companionRemote.session.noSession' => 'Geen sessie beschikbaar',
'companionRemote.session.scanQrCode' => 'Scan QR-code',
'companionRemote.session.orEnterManually' => 'Of voer handmatig in',
'companionRemote.session.hostAddress' => 'Hostadres', 'companionRemote.session.hostAddress' => 'Hostadres',
'companionRemote.session.sessionId' => 'Sessie-ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Verbonden', 'companionRemote.session.connected' => 'Verbonden',
'companionRemote.session.waitingForConnection' => 'Wachten op verbinding...', 'companionRemote.session.serverRunning' => 'Externe server actief',
'companionRemote.session.serverStopped' => 'Externe server gestopt',
'companionRemote.session.serverRunningDescription' => 'Mobiele apparaten op je netwerk kunnen deze app ontdekken en ermee verbinden',
'companionRemote.session.serverStoppedDescription' => 'Start de server om mobiele apparaten te laten verbinden',
'companionRemote.session.usePhoneToControl' => 'Gebruik je mobiele apparaat om deze app te bedienen', 'companionRemote.session.usePhoneToControl' => 'Gebruik je mobiele apparaat om deze app te bedienen',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} gekopieerd naar klembord', 'companionRemote.session.startServer' => 'Server starten',
'companionRemote.session.copyToClipboard' => 'Kopieer naar klembord', 'companionRemote.session.stopServer' => 'Server stoppen',
'companionRemote.session.newSession' => 'Nieuwe sessie',
'companionRemote.session.minimize' => 'Minimaliseren', 'companionRemote.session.minimize' => 'Minimaliseren',
'companionRemote.pairing.scan' => 'Scannen', 'companionRemote.pairing.pairWithDesktop' => 'Verbinden met desktop',
'companionRemote.pairing.manual' => 'Handmatig', 'companionRemote.pairing.discoveryDescription' => 'Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch',
'companionRemote.pairing.pairWithDesktop' => 'Koppelen met desktop',
'companionRemote.pairing.enterSessionDetails' => 'Voer de sessiegegevens in die op je desktop-apparaat worden getoond',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Voer 8-tekens sessie-ID in',
'companionRemote.pairing.pinHint' => 'Voer 6-cijferige PIN in',
'companionRemote.pairing.connecting' => 'Verbinden...', 'companionRemote.pairing.connecting' => 'Verbinden...',
'companionRemote.pairing.tips' => 'Tips', 'companionRemote.pairing.searchingForDevices' => 'Apparaten zoeken...',
'companionRemote.pairing.tipDesktop' => 'Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu', 'companionRemote.pairing.noDevicesFound' => 'Geen apparaten gevonden op je netwerk',
'companionRemote.pairing.tipScan' => 'Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen', 'companionRemote.pairing.noDevicesHint' => 'Zorg ervoor dat Plezy geopend is op je desktop en dat beide apparaten op hetzelfde WiFi-netwerk zitten',
'companionRemote.pairing.tipWifi' => 'Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten', 'companionRemote.pairing.availableDevices' => 'Beschikbare apparaten',
'companionRemote.pairing.cameraPermissionRequired' => 'Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.', 'companionRemote.pairing.manualConnection' => 'Handmatige verbinding',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kan camera niet starten: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.',
'companionRemote.pairing.scanInstruction' => 'Richt je camera op de QR-code die op je desktop wordt getoond', 'companionRemote.pairing.validationHostRequired' => 'Voer het hostadres in',
'companionRemote.pairing.invalidQrCode' => 'Ongeldig QR-codeformaat',
'companionRemote.pairing.validationHostRequired' => 'Voer een hostadres in',
'companionRemote.pairing.validationHostFormat' => 'Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Voer een sessie-ID in', 'companionRemote.pairing.connectionTimedOut' => 'Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.',
'companionRemote.pairing.validationSessionIdLength' => 'Sessie-ID moet 8 tekens zijn', 'companionRemote.pairing.sessionNotFound' => 'Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.',
'companionRemote.pairing.validationPinRequired' => 'Voer een PIN in', 'companionRemote.pairing.authFailed' => 'Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.',
'companionRemote.pairing.validationPinLength' => 'PIN moet 6 cijfers zijn', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kan niet verbinden: ${error}',
'companionRemote.pairing.connectionTimedOut' => 'Verbinding verlopen. Controleer de sessie-ID en PIN.',
'companionRemote.pairing.sessionNotFound' => 'Kan de sessie niet vinden. Controleer je gegevens.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Verbinden mislukt: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Wil je de verbinding met de externe sessie verbreken?', 'companionRemote.remote.disconnectConfirm' => 'Wil je de verbinding met de externe sessie verbreken?',
'companionRemote.remote.reconnecting' => 'Opnieuw verbinden...', 'companionRemote.remote.reconnecting' => 'Opnieuw verbinden...',
'companionRemote.remote.attemptOf' => ({required Object current}) => 'Poging ${current} van 5', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Poging ${current} van 5',
+46 -66
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsPl implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Serwer zdalnego sterowania';
@override String get companionRemoteServerDescription => 'Pozwól urządzeniom mobilnym w sieci sterować tą aplikacją';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionPl implements TranslationsCompanionRemo
final TranslationsPl _root; // ignore: unused_field final TranslationsPl _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Tworzenie sesji zdalnej...'; @override String get startingServer => 'Uruchamianie serwera zdalnego...';
@override String get failedToCreate => 'Nie udało się utworzyć sesji zdalnej:'; @override String get failedToCreate => 'Nie udało się uruchomić serwera zdalnego:';
@override String get noSession => 'Brak dostępnej sesji';
@override String get scanQrCode => 'Zeskanuj kod QR';
@override String get orEnterManually => 'Lub wprowadź ręcznie';
@override String get hostAddress => 'Adres hosta'; @override String get hostAddress => 'Adres hosta';
@override String get sessionId => 'ID sesji';
@override String get pin => 'PIN';
@override String get connected => 'Połączono'; @override String get connected => 'Połączono';
@override String get waitingForConnection => 'Oczekiwanie na połączenie...'; @override String get serverRunning => 'Serwer zdalny aktywny';
@override String get usePhoneToControl => 'Użyj urządzenia mobilnego do sterowania tą aplikacją'; @override String get serverStopped => 'Serwer zdalny zatrzymany';
@override String copiedToClipboard({required Object label}) => '${label} skopiowane do schowka'; @override String get serverRunningDescription => 'Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć';
@override String get copyToClipboard => 'Kopiuj do schowka'; @override String get serverStoppedDescription => 'Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych';
@override String get newSession => 'Nowa sesja'; @override String get usePhoneToControl => 'Użyj urządzenia mobilnego, aby sterować tą aplikacją';
@override String get startServer => 'Uruchom serwer';
@override String get stopServer => 'Zatrzymaj serwer';
@override String get minimize => 'Minimalizuj'; @override String get minimize => 'Minimalizuj';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingPl implements TranslationsCompanionRemo
final TranslationsPl _root; // ignore: unused_field final TranslationsPl _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Skanuj'; @override String get pairWithDesktop => 'Połącz z komputerem';
@override String get manual => 'cznie'; @override String get discoveryDescription => 'Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie';
@override String get pairWithDesktop => 'Sparuj z komputerem';
@override String get enterSessionDetails => 'Wprowadź dane sesji wyświetlone na urządzeniu desktop';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Wprowadź 8-znakowe ID sesji';
@override String get pinHint => 'Wprowadź 6-cyfrowy PIN';
@override String get connecting => 'Łączenie...'; @override String get connecting => 'Łączenie...';
@override String get tips => 'Wskazówki'; @override String get searchingForDevices => 'Szukanie urządzeń...';
@override String get tipDesktop => 'Otwórz Plezy na komputerze i włącz Pilota zdalnego w ustawieniach lub menu'; @override String get noDevicesFound => 'Nie znaleziono urządzeń w sieci';
@override String get tipScan => 'Użyj zakładki Skanuj, aby szybko sparować skanując kod QR na komputerze'; @override String get noDevicesHint => 'Upewnij się, że Plezy jest otwarte na komputerze i oba urządzenia są w tej samej sieci WiFi';
@override String get tipWifi => 'Upewnij się, że oba urządzenia są w tej samej sieci WiFi'; @override String get availableDevices => 'Dostępne urządzenia';
@override String get cameraPermissionRequired => 'Uprawnienie do kamery jest wymagane do skanowania kodów QR.\nPrzyznaj dostęp do kamery w ustawieniach urządzenia.'; @override String get manualConnection => 'Połączenie ręczne';
@override String cameraError({required Object error}) => 'Nie udało się uruchomić kamery: ${error}'; @override String get cryptoInitFailed => 'Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.';
@override String get scanInstruction => 'Skieruj kamerę na kod QR wyświetlony na komputerze';
@override String get invalidQrCode => 'Nieprawidłowy format kodu QR';
@override String get validationHostRequired => 'Wprowadź adres hosta'; @override String get validationHostRequired => 'Wprowadź adres hosta';
@override String get validationHostFormat => 'Format musi być IP:port (np. 192.168.1.100:48632)'; @override String get validationHostFormat => 'Format musi być IP:port (np. 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Wprowadź ID sesji'; @override String get connectionTimedOut => 'Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.';
@override String get validationSessionIdLength => 'ID sesji musi mieć 8 znaków'; @override String get sessionNotFound => 'Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.';
@override String get validationPinRequired => 'Wprowadź PIN'; @override String get authFailed => 'Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.';
@override String get validationPinLength => 'PIN musi mieć 6 cyfr';
@override String get connectionTimedOut => 'Upłynął czas połączenia. Sprawdź ID sesji i PIN.';
@override String get sessionNotFound => 'Nie znaleziono sesji. Sprawdź swoje dane.';
@override String failedToConnect({required Object error}) => 'Nie udało się połączyć: ${error}'; @override String failedToConnect({required Object error}) => 'Nie udało się połączyć: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsPl {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Serwer zdalnego sterowania',
'settings.companionRemoteServerDescription' => 'Pozwól urządzeniom mobilnym w sieci sterować tą aplikacją',
'search.hint' => 'Szukaj filmów, seriali, muzyki...', 'search.hint' => 'Szukaj filmów, seriali, muzyki...',
'search.tryDifferentTerm' => 'Spróbuj innego wyszukiwania', 'search.tryDifferentTerm' => 'Spróbuj innego wyszukiwania',
'search.searchYourMedia' => 'Przeszukaj swoje media', 'search.searchYourMedia' => 'Przeszukaj swoje media',
@@ -1881,10 +1873,10 @@ extension on TranslationsPl {
'libraries.groupings.seasons' => 'Sezony', 'libraries.groupings.seasons' => 'Sezony',
'libraries.groupings.episodes' => 'Odcinki', 'libraries.groupings.episodes' => 'Odcinki',
'libraries.groupings.folders' => 'Foldery', 'libraries.groupings.folders' => 'Foldery',
'about.title' => 'O aplikacji',
'about.openSourceLicenses' => 'Licencje open source',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'O aplikacji',
'about.openSourceLicenses' => 'Licencje open source',
'about.versionLabel' => ({required Object version}) => 'Wersja ${version}', 'about.versionLabel' => ({required Object version}) => 'Wersja ${version}',
'about.appDescription' => 'Piękny klient Plex na Flutter', 'about.appDescription' => 'Piękny klient Plex na Flutter',
'about.viewLicensesDescription' => 'Zobacz licencje bibliotek zewnętrznych', 'about.viewLicensesDescription' => 'Zobacz licencje bibliotek zewnętrznych',
@@ -2076,45 +2068,33 @@ extension on TranslationsPl {
'companionRemote.remoteControl' => 'Pilot zdalny', 'companionRemote.remoteControl' => 'Pilot zdalny',
'companionRemote.controlDesktop' => 'Steruj urządzeniem desktop', 'companionRemote.controlDesktop' => 'Steruj urządzeniem desktop',
'companionRemote.connectedTo' => ({required Object name}) => 'Połączono z ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Połączono z ${name}',
'companionRemote.session.creatingSession' => 'Tworzenie sesji zdalnej...', 'companionRemote.session.startingServer' => 'Uruchamianie serwera zdalnego...',
'companionRemote.session.failedToCreate' => 'Nie udało się utworzyć sesji zdalnej:', 'companionRemote.session.failedToCreate' => 'Nie udało się uruchomić serwera zdalnego:',
'companionRemote.session.noSession' => 'Brak dostępnej sesji',
'companionRemote.session.scanQrCode' => 'Zeskanuj kod QR',
'companionRemote.session.orEnterManually' => 'Lub wprowadź ręcznie',
'companionRemote.session.hostAddress' => 'Adres hosta', 'companionRemote.session.hostAddress' => 'Adres hosta',
'companionRemote.session.sessionId' => 'ID sesji',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Połączono', 'companionRemote.session.connected' => 'Połączono',
'companionRemote.session.waitingForConnection' => 'Oczekiwanie na połączenie...', 'companionRemote.session.serverRunning' => 'Serwer zdalny aktywny',
'companionRemote.session.usePhoneToControl' => 'Użyj urządzenia mobilnego do sterowania tą aplikacją', 'companionRemote.session.serverStopped' => 'Serwer zdalny zatrzymany',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} skopiowane do schowka', 'companionRemote.session.serverRunningDescription' => 'Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć',
'companionRemote.session.copyToClipboard' => 'Kopiuj do schowka', 'companionRemote.session.serverStoppedDescription' => 'Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych',
'companionRemote.session.newSession' => 'Nowa sesja', 'companionRemote.session.usePhoneToControl' => 'Użyj urządzenia mobilnego, aby sterować tą aplikacją',
'companionRemote.session.startServer' => 'Uruchom serwer',
'companionRemote.session.stopServer' => 'Zatrzymaj serwer',
'companionRemote.session.minimize' => 'Minimalizuj', 'companionRemote.session.minimize' => 'Minimalizuj',
'companionRemote.pairing.scan' => 'Skanuj', 'companionRemote.pairing.pairWithDesktop' => 'Połącz z komputerem',
'companionRemote.pairing.manual' => 'cznie', 'companionRemote.pairing.discoveryDescription' => 'Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie',
'companionRemote.pairing.pairWithDesktop' => 'Sparuj z komputerem',
'companionRemote.pairing.enterSessionDetails' => 'Wprowadź dane sesji wyświetlone na urządzeniu desktop',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Wprowadź 8-znakowe ID sesji',
'companionRemote.pairing.pinHint' => 'Wprowadź 6-cyfrowy PIN',
'companionRemote.pairing.connecting' => 'Łączenie...', 'companionRemote.pairing.connecting' => 'Łączenie...',
'companionRemote.pairing.tips' => 'Wskazówki', 'companionRemote.pairing.searchingForDevices' => 'Szukanie urządzeń...',
'companionRemote.pairing.tipDesktop' => 'Otwórz Plezy na komputerze i włącz Pilota zdalnego w ustawieniach lub menu', 'companionRemote.pairing.noDevicesFound' => 'Nie znaleziono urządzeń w sieci',
'companionRemote.pairing.tipScan' => 'Użyj zakładki Skanuj, aby szybko sparować skanując kod QR na komputerze', 'companionRemote.pairing.noDevicesHint' => 'Upewnij się, że Plezy jest otwarte na komputerze i oba urządzenia są w tej samej sieci WiFi',
'companionRemote.pairing.tipWifi' => 'Upewnij się, że oba urządzenia są w tej samej sieci WiFi', 'companionRemote.pairing.availableDevices' => 'Dostępne urządzenia',
'companionRemote.pairing.cameraPermissionRequired' => 'Uprawnienie do kamery jest wymagane do skanowania kodów QR.\nPrzyznaj dostęp do kamery w ustawieniach urządzenia.', 'companionRemote.pairing.manualConnection' => 'Połączenie ręczne',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Nie udało się uruchomić kamery: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.',
'companionRemote.pairing.scanInstruction' => 'Skieruj kamerę na kod QR wyświetlony na komputerze',
'companionRemote.pairing.invalidQrCode' => 'Nieprawidłowy format kodu QR',
'companionRemote.pairing.validationHostRequired' => 'Wprowadź adres hosta', 'companionRemote.pairing.validationHostRequired' => 'Wprowadź adres hosta',
'companionRemote.pairing.validationHostFormat' => 'Format musi być IP:port (np. 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Format musi być IP:port (np. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Wprowadź ID sesji', 'companionRemote.pairing.connectionTimedOut' => 'Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.',
'companionRemote.pairing.validationSessionIdLength' => 'ID sesji musi mieć 8 znaków', 'companionRemote.pairing.sessionNotFound' => 'Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.',
'companionRemote.pairing.validationPinRequired' => 'Wprowadź PIN', 'companionRemote.pairing.authFailed' => 'Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.',
'companionRemote.pairing.validationPinLength' => 'PIN musi mieć 6 cyfr',
'companionRemote.pairing.connectionTimedOut' => 'Upłynął czas połączenia. Sprawdź ID sesji i PIN.',
'companionRemote.pairing.sessionNotFound' => 'Nie znaleziono sesji. Sprawdź swoje dane.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Nie udało się połączyć: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Nie udało się połączyć: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Czy chcesz się rozłączyć od sesji zdalnej?', 'companionRemote.remote.disconnectConfirm' => 'Czy chcesz się rozłączyć od sesji zdalnej?',
'companionRemote.remote.reconnecting' => 'Ponowne łączenie...', 'companionRemote.remote.reconnecting' => 'Ponowne łączenie...',
+54 -74
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsPt implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Servidor de controlo remoto';
@override String get companionRemoteServerDescription => 'Permitir que dispositivos móveis na sua rede controlem esta aplicação';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionPt implements TranslationsCompanionRemo
final TranslationsPt _root; // ignore: unused_field final TranslationsPt _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Criando sessão remota...'; @override String get startingServer => 'A iniciar servidor remoto...';
@override String get failedToCreate => 'Falha ao criar sessão remota:'; @override String get failedToCreate => 'Falha ao iniciar o servidor remoto:';
@override String get noSession => 'Nenhuma sessão disponível'; @override String get hostAddress => 'Endereço do host';
@override String get scanQrCode => 'Escanear QR Code';
@override String get orEnterManually => 'Ou insira manualmente';
@override String get hostAddress => 'Endereço do Host';
@override String get sessionId => 'ID da Sessão';
@override String get pin => 'PIN';
@override String get connected => 'Conectado'; @override String get connected => 'Conectado';
@override String get waitingForConnection => 'Aguardando conexão...'; @override String get serverRunning => 'Servidor remoto ativo';
@override String get usePhoneToControl => 'Use seu dispositivo móvel para controlar este app'; @override String get serverStopped => 'Servidor remoto parado';
@override String copiedToClipboard({required Object label}) => '${label} copiado para a área de transferência'; @override String get serverRunningDescription => 'Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação';
@override String get copyToClipboard => 'Copiar para área de transferência'; @override String get serverStoppedDescription => 'Inicie o servidor para permitir que dispositivos móveis se conectem';
@override String get newSession => 'Nova Sessão'; @override String get usePhoneToControl => 'Use o seu dispositivo móvel para controlar esta aplicação';
@override String get startServer => 'Iniciar servidor';
@override String get stopServer => 'Parar servidor';
@override String get minimize => 'Minimizar'; @override String get minimize => 'Minimizar';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingPt implements TranslationsCompanionRemo
final TranslationsPt _root; // ignore: unused_field final TranslationsPt _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Escanear'; @override String get pairWithDesktop => 'Conectar ao desktop';
@override String get manual => 'Manual'; @override String get discoveryDescription => 'Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente';
@override String get pairWithDesktop => 'Parear com Desktop';
@override String get enterSessionDetails => 'Insira os detalhes da sessão exibidos no seu dispositivo desktop';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Insira o ID da sessão de 8 caracteres'; @override String get connecting => 'A conectar...';
@override String get pinHint => 'Insira o PIN de 6 dígitos'; @override String get searchingForDevices => 'A procurar dispositivos...';
@override String get connecting => 'Conectando...'; @override String get noDevicesFound => 'Nenhum dispositivo encontrado na sua rede';
@override String get tips => 'Dicas'; @override String get noDevicesHint => 'Certifique-se de que o Plezy está aberto no seu desktop e que ambos os dispositivos estão na mesma rede WiFi';
@override String get tipDesktop => 'Abra o Plezy no desktop e ative o Controle Remoto nas configurações ou menu'; @override String get availableDevices => 'Dispositivos disponíveis';
@override String get tipScan => 'Use a aba Escanear para parear rapidamente escaneando o QR code no seu desktop'; @override String get manualConnection => 'Conexão manual';
@override String get tipWifi => 'Certifique-se de que ambos os dispositivos estão na mesma rede WiFi'; @override String get cryptoInitFailed => 'Não foi possível inicializar a conexão segura. Certifique-se de que está conectado a uma conta Plex.';
@override String get cameraPermissionRequired => 'A permissão da câmera é necessária para escanear QR codes.\nConceda acesso à câmera nas configurações do dispositivo.'; @override String get validationHostRequired => 'Introduza o endereço do host';
@override String cameraError({required Object error}) => 'Não foi possível iniciar a câmera: ${error}'; @override String get validationHostFormat => 'O formato deve ser IP:porta (ex. 192.168.1.100:48632)';
@override String get scanInstruction => 'Aponte sua câmera para o QR code exibido no seu desktop'; @override String get connectionTimedOut => 'Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.';
@override String get invalidQrCode => 'Formato de QR code inválido'; @override String get sessionNotFound => 'Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.';
@override String get validationHostRequired => 'Insira o endereço do host'; @override String get authFailed => 'Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.';
@override String get validationHostFormat => 'O formato deve ser IP:porta (ex: 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Insira um ID de sessão';
@override String get validationSessionIdLength => 'O ID da sessão deve ter 8 caracteres';
@override String get validationPinRequired => 'Insira um PIN';
@override String get validationPinLength => 'O PIN deve ter 6 dígitos';
@override String get connectionTimedOut => 'Tempo de conexão esgotado. Verifique o ID da sessão e o PIN.';
@override String get sessionNotFound => 'Sessão não encontrada. Verifique suas credenciais.';
@override String failedToConnect({required Object error}) => 'Falha ao conectar: ${error}'; @override String failedToConnect({required Object error}) => 'Falha ao conectar: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsPt {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Servidor de controlo remoto',
'settings.companionRemoteServerDescription' => 'Permitir que dispositivos móveis na sua rede controlem esta aplicação',
'search.hint' => 'Buscar filmes, séries, músicas...', 'search.hint' => 'Buscar filmes, séries, músicas...',
'search.tryDifferentTerm' => 'Tente um termo de busca diferente', 'search.tryDifferentTerm' => 'Tente um termo de busca diferente',
'search.searchYourMedia' => 'Buscar suas mídias', 'search.searchYourMedia' => 'Buscar suas mídias',
@@ -1881,10 +1873,10 @@ extension on TranslationsPt {
'libraries.groupings.seasons' => 'Temporadas', 'libraries.groupings.seasons' => 'Temporadas',
'libraries.groupings.episodes' => 'Episódios', 'libraries.groupings.episodes' => 'Episódios',
'libraries.groupings.folders' => 'Pastas', 'libraries.groupings.folders' => 'Pastas',
'about.title' => 'Sobre',
'about.openSourceLicenses' => 'Licenças Open Source',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Sobre',
'about.openSourceLicenses' => 'Licenças Open Source',
'about.versionLabel' => ({required Object version}) => 'Versão ${version}', 'about.versionLabel' => ({required Object version}) => 'Versão ${version}',
'about.appDescription' => 'Um belo cliente Plex para Flutter', 'about.appDescription' => 'Um belo cliente Plex para Flutter',
'about.viewLicensesDescription' => 'Ver licenças de bibliotecas de terceiros', 'about.viewLicensesDescription' => 'Ver licenças de bibliotecas de terceiros',
@@ -2076,45 +2068,33 @@ extension on TranslationsPt {
'companionRemote.remoteControl' => 'Controle Remoto', 'companionRemote.remoteControl' => 'Controle Remoto',
'companionRemote.controlDesktop' => 'Controlar um dispositivo desktop', 'companionRemote.controlDesktop' => 'Controlar um dispositivo desktop',
'companionRemote.connectedTo' => ({required Object name}) => 'Conectado a ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Conectado a ${name}',
'companionRemote.session.creatingSession' => 'Criando sessão remota...', 'companionRemote.session.startingServer' => 'A iniciar servidor remoto...',
'companionRemote.session.failedToCreate' => 'Falha ao criar sessão remota:', 'companionRemote.session.failedToCreate' => 'Falha ao iniciar o servidor remoto:',
'companionRemote.session.noSession' => 'Nenhuma sessão disponível', 'companionRemote.session.hostAddress' => 'Endereço do host',
'companionRemote.session.scanQrCode' => 'Escanear QR Code',
'companionRemote.session.orEnterManually' => 'Ou insira manualmente',
'companionRemote.session.hostAddress' => 'Endereço do Host',
'companionRemote.session.sessionId' => 'ID da Sessão',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Conectado', 'companionRemote.session.connected' => 'Conectado',
'companionRemote.session.waitingForConnection' => 'Aguardando conexão...', 'companionRemote.session.serverRunning' => 'Servidor remoto ativo',
'companionRemote.session.usePhoneToControl' => 'Use seu dispositivo móvel para controlar este app', 'companionRemote.session.serverStopped' => 'Servidor remoto parado',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} copiado para a área de transferência', 'companionRemote.session.serverRunningDescription' => 'Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação',
'companionRemote.session.copyToClipboard' => 'Copiar para área de transferência', 'companionRemote.session.serverStoppedDescription' => 'Inicie o servidor para permitir que dispositivos móveis se conectem',
'companionRemote.session.newSession' => 'Nova Sessão', 'companionRemote.session.usePhoneToControl' => 'Use o seu dispositivo móvel para controlar esta aplicação',
'companionRemote.session.startServer' => 'Iniciar servidor',
'companionRemote.session.stopServer' => 'Parar servidor',
'companionRemote.session.minimize' => 'Minimizar', 'companionRemote.session.minimize' => 'Minimizar',
'companionRemote.pairing.scan' => 'Escanear', 'companionRemote.pairing.pairWithDesktop' => 'Conectar ao desktop',
'companionRemote.pairing.manual' => 'Manual', 'companionRemote.pairing.discoveryDescription' => 'Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente',
'companionRemote.pairing.pairWithDesktop' => 'Parear com Desktop',
'companionRemote.pairing.enterSessionDetails' => 'Insira os detalhes da sessão exibidos no seu dispositivo desktop',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Insira o ID da sessão de 8 caracteres', 'companionRemote.pairing.connecting' => 'A conectar...',
'companionRemote.pairing.pinHint' => 'Insira o PIN de 6 dígitos', 'companionRemote.pairing.searchingForDevices' => 'A procurar dispositivos...',
'companionRemote.pairing.connecting' => 'Conectando...', 'companionRemote.pairing.noDevicesFound' => 'Nenhum dispositivo encontrado na sua rede',
'companionRemote.pairing.tips' => 'Dicas', 'companionRemote.pairing.noDevicesHint' => 'Certifique-se de que o Plezy está aberto no seu desktop e que ambos os dispositivos estão na mesma rede WiFi',
'companionRemote.pairing.tipDesktop' => 'Abra o Plezy no desktop e ative o Controle Remoto nas configurações ou menu', 'companionRemote.pairing.availableDevices' => 'Dispositivos disponíveis',
'companionRemote.pairing.tipScan' => 'Use a aba Escanear para parear rapidamente escaneando o QR code no seu desktop', 'companionRemote.pairing.manualConnection' => 'Conexão manual',
'companionRemote.pairing.tipWifi' => 'Certifique-se de que ambos os dispositivos estão na mesma rede WiFi', 'companionRemote.pairing.cryptoInitFailed' => 'Não foi possível inicializar a conexão segura. Certifique-se de que está conectado a uma conta Plex.',
'companionRemote.pairing.cameraPermissionRequired' => 'A permissão da câmera é necessária para escanear QR codes.\nConceda acesso à câmera nas configurações do dispositivo.', 'companionRemote.pairing.validationHostRequired' => 'Introduza o endereço do host',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Não foi possível iniciar a câmera: ${error}', 'companionRemote.pairing.validationHostFormat' => 'O formato deve ser IP:porta (ex. 192.168.1.100:48632)',
'companionRemote.pairing.scanInstruction' => 'Aponte sua câmera para o QR code exibido no seu desktop', 'companionRemote.pairing.connectionTimedOut' => 'Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.',
'companionRemote.pairing.invalidQrCode' => 'Formato de QR code inválido', 'companionRemote.pairing.sessionNotFound' => 'Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.',
'companionRemote.pairing.validationHostRequired' => 'Insira o endereço do host', 'companionRemote.pairing.authFailed' => 'Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.',
'companionRemote.pairing.validationHostFormat' => 'O formato deve ser IP:porta (ex: 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Insira um ID de sessão',
'companionRemote.pairing.validationSessionIdLength' => 'O ID da sessão deve ter 8 caracteres',
'companionRemote.pairing.validationPinRequired' => 'Insira um PIN',
'companionRemote.pairing.validationPinLength' => 'O PIN deve ter 6 dígitos',
'companionRemote.pairing.connectionTimedOut' => 'Tempo de conexão esgotado. Verifique o ID da sessão e o PIN.',
'companionRemote.pairing.sessionNotFound' => 'Sessão não encontrada. Verifique suas credenciais.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Falha ao conectar: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Falha ao conectar: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Deseja desconectar da sessão remota?', 'companionRemote.remote.disconnectConfirm' => 'Deseja desconectar da sessão remota?',
'companionRemote.remote.reconnecting' => 'Reconectando...', 'companionRemote.remote.reconnecting' => 'Reconectando...',
+44 -64
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsRu implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Сервер удалённого управления';
@override String get companionRemoteServerDescription => 'Разрешить мобильным устройствам в сети управлять этим приложением';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionRu implements TranslationsCompanionRemo
final TranslationsRu _root; // ignore: unused_field final TranslationsRu _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Создание удалённой сессии...'; @override String get startingServer => 'Запуск удалённого сервера...';
@override String get failedToCreate => 'Не удалось создать удалённую сессию:'; @override String get failedToCreate => 'Не удалось запустить удалённый сервер:';
@override String get noSession => 'Нет доступных сессий';
@override String get scanQrCode => 'Сканировать QR-код';
@override String get orEnterManually => 'Или введите вручную';
@override String get hostAddress => 'Адрес хоста'; @override String get hostAddress => 'Адрес хоста';
@override String get sessionId => 'ID сессии';
@override String get pin => 'PIN';
@override String get connected => 'Подключено'; @override String get connected => 'Подключено';
@override String get waitingForConnection => 'Ожидание подключения...'; @override String get serverRunning => 'Удалённый сервер активен';
@override String get serverStopped => 'Удалённый сервер остановлен';
@override String get serverRunningDescription => 'Мобильные устройства в вашей сети могут обнаруживать это приложение и подключаться к нему';
@override String get serverStoppedDescription => 'Запустите сервер, чтобы разрешить подключение мобильных устройств';
@override String get usePhoneToControl => 'Используйте мобильное устройство для управления этим приложением'; @override String get usePhoneToControl => 'Используйте мобильное устройство для управления этим приложением';
@override String copiedToClipboard({required Object label}) => '${label} скопировано в буфер обмена'; @override String get startServer => 'Запустить сервер';
@override String get copyToClipboard => 'Скопировать в буфер обмена'; @override String get stopServer => 'Остановить сервер';
@override String get newSession => 'Новая сессия';
@override String get minimize => 'Свернуть'; @override String get minimize => 'Свернуть';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingRu implements TranslationsCompanionRemo
final TranslationsRu _root; // ignore: unused_field final TranslationsRu _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Сканировать'; @override String get pairWithDesktop => 'Подключиться к компьютеру';
@override String get manual => 'Вручную'; @override String get discoveryDescription => 'Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически';
@override String get pairWithDesktop => 'Сопряжение с ПК';
@override String get enterSessionDetails => 'Введите данные сессии, показанные на десктопном устройстве';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Введите 8-символьный ID сессии';
@override String get pinHint => 'Введите 6-значный PIN';
@override String get connecting => 'Подключение...'; @override String get connecting => 'Подключение...';
@override String get tips => 'Советы'; @override String get searchingForDevices => 'Поиск устройств...';
@override String get tipDesktop => 'Откройте Plezy на десктопе и включите пульт управления в настройках или меню'; @override String get noDevicesFound => 'Устройства в вашей сети не найдены';
@override String get tipScan => 'Используйте вкладку «Сканировать» для быстрого сопряжения через QR-код на десктопе'; @override String get noDevicesHint => 'Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi';
@override String get tipWifi => 'Убедитесь, что оба устройства находятся в одной WiFi-сети'; @override String get availableDevices => 'Доступные устройства';
@override String get cameraPermissionRequired => 'Для сканирования QR-кодов требуется разрешение на камеру.\nПредоставьте доступ к камере в настройках устройства.'; @override String get manualConnection => 'Ручное подключение';
@override String cameraError({required Object error}) => 'Не удалось запустить камеру: ${error}'; @override String get cryptoInitFailed => 'Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.';
@override String get scanInstruction => 'Наведите камеру на QR-код, показанный на десктопе';
@override String get invalidQrCode => 'Недопустимый формат QR-кода';
@override String get validationHostRequired => 'Введите адрес хоста'; @override String get validationHostRequired => 'Введите адрес хоста';
@override String get validationHostFormat => 'Формат должен быть IP:порт (например, 192.168.1.100:48632)'; @override String get validationHostFormat => 'Формат должен быть IP:порт (например, 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Введите ID сессии'; @override String get connectionTimedOut => 'Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.';
@override String get validationSessionIdLength => 'ID сессии должен содержать 8 символов'; @override String get sessionNotFound => 'Устройство не найдено. Убедитесь, что Plezy запущен на хосте.';
@override String get validationPinRequired => 'Введите PIN'; @override String get authFailed => 'Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.';
@override String get validationPinLength => 'PIN должен содержать 6 цифр';
@override String get connectionTimedOut => 'Время подключения истекло. Проверьте ID сессии и PIN.';
@override String get sessionNotFound => 'Сессия не найдена. Проверьте ваши учётные данные.';
@override String failedToConnect({required Object error}) => 'Не удалось подключиться: ${error}'; @override String failedToConnect({required Object error}) => 'Не удалось подключиться: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsRu {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Сервер удалённого управления',
'settings.companionRemoteServerDescription' => 'Разрешить мобильным устройствам в сети управлять этим приложением',
'search.hint' => 'Поиск фильмов, сериалов, музыки...', 'search.hint' => 'Поиск фильмов, сериалов, музыки...',
'search.tryDifferentTerm' => 'Попробуйте другой запрос', 'search.tryDifferentTerm' => 'Попробуйте другой запрос',
'search.searchYourMedia' => 'Поиск в вашей медиатеке', 'search.searchYourMedia' => 'Поиск в вашей медиатеке',
@@ -1881,10 +1873,10 @@ extension on TranslationsRu {
'libraries.groupings.seasons' => 'Сезоны', 'libraries.groupings.seasons' => 'Сезоны',
'libraries.groupings.episodes' => 'Эпизоды', 'libraries.groupings.episodes' => 'Эпизоды',
'libraries.groupings.folders' => 'Папки', 'libraries.groupings.folders' => 'Папки',
'about.title' => 'О приложении',
'about.openSourceLicenses' => 'Лицензии открытого ПО',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'О приложении',
'about.openSourceLicenses' => 'Лицензии открытого ПО',
'about.versionLabel' => ({required Object version}) => 'Версия ${version}', 'about.versionLabel' => ({required Object version}) => 'Версия ${version}',
'about.appDescription' => 'Красивый клиент Plex на Flutter', 'about.appDescription' => 'Красивый клиент Plex на Flutter',
'about.viewLicensesDescription' => 'Просмотр лицензий сторонних библиотек', 'about.viewLicensesDescription' => 'Просмотр лицензий сторонних библиотек',
@@ -2076,45 +2068,33 @@ extension on TranslationsRu {
'companionRemote.remoteControl' => 'Пульт управления', 'companionRemote.remoteControl' => 'Пульт управления',
'companionRemote.controlDesktop' => 'Управлять десктопным устройством', 'companionRemote.controlDesktop' => 'Управлять десктопным устройством',
'companionRemote.connectedTo' => ({required Object name}) => 'Подключено к ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Подключено к ${name}',
'companionRemote.session.creatingSession' => 'Создание удалённой сессии...', 'companionRemote.session.startingServer' => 'Запуск удалённого сервера...',
'companionRemote.session.failedToCreate' => 'Не удалось создать удалённую сессию:', 'companionRemote.session.failedToCreate' => 'Не удалось запустить удалённый сервер:',
'companionRemote.session.noSession' => 'Нет доступных сессий',
'companionRemote.session.scanQrCode' => 'Сканировать QR-код',
'companionRemote.session.orEnterManually' => 'Или введите вручную',
'companionRemote.session.hostAddress' => 'Адрес хоста', 'companionRemote.session.hostAddress' => 'Адрес хоста',
'companionRemote.session.sessionId' => 'ID сессии',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Подключено', 'companionRemote.session.connected' => 'Подключено',
'companionRemote.session.waitingForConnection' => 'Ожидание подключения...', 'companionRemote.session.serverRunning' => 'Удалённый сервер активен',
'companionRemote.session.serverStopped' => 'Удалённый сервер остановлен',
'companionRemote.session.serverRunningDescription' => 'Мобильные устройства в вашей сети могут обнаруживать это приложение и подключаться к нему',
'companionRemote.session.serverStoppedDescription' => 'Запустите сервер, чтобы разрешить подключение мобильных устройств',
'companionRemote.session.usePhoneToControl' => 'Используйте мобильное устройство для управления этим приложением', 'companionRemote.session.usePhoneToControl' => 'Используйте мобильное устройство для управления этим приложением',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} скопировано в буфер обмена', 'companionRemote.session.startServer' => 'Запустить сервер',
'companionRemote.session.copyToClipboard' => 'Скопировать в буфер обмена', 'companionRemote.session.stopServer' => 'Остановить сервер',
'companionRemote.session.newSession' => 'Новая сессия',
'companionRemote.session.minimize' => 'Свернуть', 'companionRemote.session.minimize' => 'Свернуть',
'companionRemote.pairing.scan' => 'Сканировать', 'companionRemote.pairing.pairWithDesktop' => 'Подключиться к компьютеру',
'companionRemote.pairing.manual' => 'Вручную', 'companionRemote.pairing.discoveryDescription' => 'Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически',
'companionRemote.pairing.pairWithDesktop' => 'Сопряжение с ПК',
'companionRemote.pairing.enterSessionDetails' => 'Введите данные сессии, показанные на десктопном устройстве',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Введите 8-символьный ID сессии',
'companionRemote.pairing.pinHint' => 'Введите 6-значный PIN',
'companionRemote.pairing.connecting' => 'Подключение...', 'companionRemote.pairing.connecting' => 'Подключение...',
'companionRemote.pairing.tips' => 'Советы', 'companionRemote.pairing.searchingForDevices' => 'Поиск устройств...',
'companionRemote.pairing.tipDesktop' => 'Откройте Plezy на десктопе и включите пульт управления в настройках или меню', 'companionRemote.pairing.noDevicesFound' => 'Устройства в вашей сети не найдены',
'companionRemote.pairing.tipScan' => 'Используйте вкладку «Сканировать» для быстрого сопряжения через QR-код на десктопе', 'companionRemote.pairing.noDevicesHint' => 'Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi',
'companionRemote.pairing.tipWifi' => 'Убедитесь, что оба устройства находятся в одной WiFi-сети', 'companionRemote.pairing.availableDevices' => 'Доступные устройства',
'companionRemote.pairing.cameraPermissionRequired' => 'Для сканирования QR-кодов требуется разрешение на камеру.\nПредоставьте доступ к камере в настройках устройства.', 'companionRemote.pairing.manualConnection' => 'Ручное подключение',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Не удалось запустить камеру: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.',
'companionRemote.pairing.scanInstruction' => 'Наведите камеру на QR-код, показанный на десктопе',
'companionRemote.pairing.invalidQrCode' => 'Недопустимый формат QR-кода',
'companionRemote.pairing.validationHostRequired' => 'Введите адрес хоста', 'companionRemote.pairing.validationHostRequired' => 'Введите адрес хоста',
'companionRemote.pairing.validationHostFormat' => 'Формат должен быть IP:порт (например, 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Формат должен быть IP:порт (например, 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Введите ID сессии', 'companionRemote.pairing.connectionTimedOut' => 'Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.',
'companionRemote.pairing.validationSessionIdLength' => 'ID сессии должен содержать 8 символов', 'companionRemote.pairing.sessionNotFound' => 'Устройство не найдено. Убедитесь, что Plezy запущен на хосте.',
'companionRemote.pairing.validationPinRequired' => 'Введите PIN', 'companionRemote.pairing.authFailed' => 'Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.',
'companionRemote.pairing.validationPinLength' => 'PIN должен содержать 6 цифр',
'companionRemote.pairing.connectionTimedOut' => 'Время подключения истекло. Проверьте ID сессии и PIN.',
'companionRemote.pairing.sessionNotFound' => 'Сессия не найдена. Проверьте ваши учётные данные.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Не удалось подключиться: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Не удалось подключиться: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Отключиться от удалённой сессии?', 'companionRemote.remote.disconnectConfirm' => 'Отключиться от удалённой сессии?',
'companionRemote.remote.reconnecting' => 'Переподключение...', 'companionRemote.remote.reconnecting' => 'Переподключение...',
+46 -66
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Companion Remote-server';
@override String get companionRemoteServerDescription => 'Tillåt mobila enheter i ditt nätverk att styra denna app';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionSv implements TranslationsCompanionRemo
final TranslationsSv _root; // ignore: unused_field final TranslationsSv _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => 'Skapar fjärrsession...'; @override String get startingServer => 'Startar fjärrserver...';
@override String get failedToCreate => 'Kunde inte skapa fjärrsession:'; @override String get failedToCreate => 'Kunde inte starta fjärrserver:';
@override String get noSession => 'Ingen session tillgänglig';
@override String get scanQrCode => 'Skanna QR-kod';
@override String get orEnterManually => 'Eller ange manuellt';
@override String get hostAddress => 'Värdadress'; @override String get hostAddress => 'Värdadress';
@override String get sessionId => 'Sessions-ID';
@override String get pin => 'PIN';
@override String get connected => 'Ansluten'; @override String get connected => 'Ansluten';
@override String get waitingForConnection => 'Väntar på anslutning...'; @override String get serverRunning => 'Fjärrserver aktiv';
@override String get serverStopped => 'Fjärrserver stoppad';
@override String get serverRunningDescription => 'Mobila enheter i ditt nätverk kan upptäcka och ansluta till denna app';
@override String get serverStoppedDescription => 'Starta servern för att tillåta mobila enheter att ansluta';
@override String get usePhoneToControl => 'Använd din mobila enhet för att styra denna app'; @override String get usePhoneToControl => 'Använd din mobila enhet för att styra denna app';
@override String copiedToClipboard({required Object label}) => '${label} kopierad till urklipp'; @override String get startServer => 'Starta server';
@override String get copyToClipboard => 'Kopiera till urklipp'; @override String get stopServer => 'Stoppa server';
@override String get newSession => 'Ny session';
@override String get minimize => 'Minimera'; @override String get minimize => 'Minimera';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingSv implements TranslationsCompanionRemo
final TranslationsSv _root; // ignore: unused_field final TranslationsSv _root; // ignore: unused_field
// Translations // Translations
@override String get scan => 'Skanna'; @override String get pairWithDesktop => 'Anslut till dator';
@override String get manual => 'Manuell'; @override String get discoveryDescription => 'Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt';
@override String get pairWithDesktop => 'Parkoppla med dator';
@override String get enterSessionDetails => 'Ange sessionsuppgifterna som visas på din datorenhet';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Ange 8-teckens sessions-ID';
@override String get pinHint => 'Ange 6-siffrig PIN';
@override String get connecting => 'Ansluter...'; @override String get connecting => 'Ansluter...';
@override String get tips => 'Tips'; @override String get searchingForDevices => 'Söker efter enheter...';
@override String get tipDesktop => 'Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny'; @override String get noDevicesFound => 'Inga enheter hittades i ditt nätverk';
@override String get tipScan => 'Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator'; @override String get noDevicesHint => 'Se till att Plezy är öppet på din dator och att båda enheterna är på samma WiFi-nätverk';
@override String get tipWifi => 'Se till att båda enheterna är på samma WiFi-nätverk'; @override String get availableDevices => 'Tillgängliga enheter';
@override String get cameraPermissionRequired => 'Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.'; @override String get manualConnection => 'Manuell anslutning';
@override String cameraError({required Object error}) => 'Kunde inte starta kameran: ${error}'; @override String get cryptoInitFailed => 'Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.';
@override String get scanInstruction => 'Rikta kameran mot QR-koden som visas på din dator'; @override String get validationHostRequired => 'Ange värdadress';
@override String get invalidQrCode => 'Ogiltigt QR-kodformat';
@override String get validationHostRequired => 'Ange en värdadress';
@override String get validationHostFormat => 'Format måste vara IP:port (t.ex. 192.168.1.100:48632)'; @override String get validationHostFormat => 'Format måste vara IP:port (t.ex. 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Ange ett sessions-ID'; @override String get connectionTimedOut => 'Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.';
@override String get validationSessionIdLength => 'Sessions-ID måste vara 8 tecken'; @override String get sessionNotFound => 'Kunde inte hitta enheten. Se till att Plezy körs på värden.';
@override String get validationPinRequired => 'Ange en PIN'; @override String get authFailed => 'Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.';
@override String get validationPinLength => 'PIN måste vara 6 siffror';
@override String get connectionTimedOut => 'Anslutningen tog för lång tid. Kontrollera sessions-ID och PIN.';
@override String get sessionNotFound => 'Kunde inte hitta sessionen. Kontrollera dina uppgifter.';
@override String failedToConnect({required Object error}) => 'Kunde inte ansluta: ${error}'; @override String failedToConnect({required Object error}) => 'Kunde inte ansluta: ${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsSv {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Companion Remote-server',
'settings.companionRemoteServerDescription' => 'Tillåt mobila enheter i ditt nätverk att styra denna app',
'search.hint' => 'Sök filmer, serier, musik...', 'search.hint' => 'Sök filmer, serier, musik...',
'search.tryDifferentTerm' => 'Prova en annan sökterm', 'search.tryDifferentTerm' => 'Prova en annan sökterm',
'search.searchYourMedia' => 'Sök i dina media', 'search.searchYourMedia' => 'Sök i dina media',
@@ -1881,10 +1873,10 @@ extension on TranslationsSv {
'libraries.groupings.seasons' => 'Säsonger', 'libraries.groupings.seasons' => 'Säsonger',
'libraries.groupings.episodes' => 'Avsnitt', 'libraries.groupings.episodes' => 'Avsnitt',
'libraries.groupings.folders' => 'Mappar', 'libraries.groupings.folders' => 'Mappar',
'about.title' => 'Om',
'about.openSourceLicenses' => 'Öppen källkod-licenser',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => 'Om',
'about.openSourceLicenses' => 'Öppen källkod-licenser',
'about.versionLabel' => ({required Object version}) => 'Version ${version}', 'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'En vacker Plex-klient för Flutter', 'about.appDescription' => 'En vacker Plex-klient för Flutter',
'about.viewLicensesDescription' => 'Visa licenser för tredjepartsbibliotek', 'about.viewLicensesDescription' => 'Visa licenser för tredjepartsbibliotek',
@@ -2076,45 +2068,33 @@ extension on TranslationsSv {
'companionRemote.remoteControl' => 'Fjärrkontroll', 'companionRemote.remoteControl' => 'Fjärrkontroll',
'companionRemote.controlDesktop' => 'Styr en datorenhet', 'companionRemote.controlDesktop' => 'Styr en datorenhet',
'companionRemote.connectedTo' => ({required Object name}) => 'Ansluten till ${name}', 'companionRemote.connectedTo' => ({required Object name}) => 'Ansluten till ${name}',
'companionRemote.session.creatingSession' => 'Skapar fjärrsession...', 'companionRemote.session.startingServer' => 'Startar fjärrserver...',
'companionRemote.session.failedToCreate' => 'Kunde inte skapa fjärrsession:', 'companionRemote.session.failedToCreate' => 'Kunde inte starta fjärrserver:',
'companionRemote.session.noSession' => 'Ingen session tillgänglig',
'companionRemote.session.scanQrCode' => 'Skanna QR-kod',
'companionRemote.session.orEnterManually' => 'Eller ange manuellt',
'companionRemote.session.hostAddress' => 'Värdadress', 'companionRemote.session.hostAddress' => 'Värdadress',
'companionRemote.session.sessionId' => 'Sessions-ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Ansluten', 'companionRemote.session.connected' => 'Ansluten',
'companionRemote.session.waitingForConnection' => 'Väntar på anslutning...', 'companionRemote.session.serverRunning' => 'Fjärrserver aktiv',
'companionRemote.session.serverStopped' => 'Fjärrserver stoppad',
'companionRemote.session.serverRunningDescription' => 'Mobila enheter i ditt nätverk kan upptäcka och ansluta till denna app',
'companionRemote.session.serverStoppedDescription' => 'Starta servern för att tillåta mobila enheter att ansluta',
'companionRemote.session.usePhoneToControl' => 'Använd din mobila enhet för att styra denna app', 'companionRemote.session.usePhoneToControl' => 'Använd din mobila enhet för att styra denna app',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} kopierad till urklipp', 'companionRemote.session.startServer' => 'Starta server',
'companionRemote.session.copyToClipboard' => 'Kopiera till urklipp', 'companionRemote.session.stopServer' => 'Stoppa server',
'companionRemote.session.newSession' => 'Ny session',
'companionRemote.session.minimize' => 'Minimera', 'companionRemote.session.minimize' => 'Minimera',
'companionRemote.pairing.scan' => 'Skanna', 'companionRemote.pairing.pairWithDesktop' => 'Anslut till dator',
'companionRemote.pairing.manual' => 'Manuell', 'companionRemote.pairing.discoveryDescription' => 'Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt',
'companionRemote.pairing.pairWithDesktop' => 'Parkoppla med dator',
'companionRemote.pairing.enterSessionDetails' => 'Ange sessionsuppgifterna som visas på din datorenhet',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Ange 8-teckens sessions-ID',
'companionRemote.pairing.pinHint' => 'Ange 6-siffrig PIN',
'companionRemote.pairing.connecting' => 'Ansluter...', 'companionRemote.pairing.connecting' => 'Ansluter...',
'companionRemote.pairing.tips' => 'Tips', 'companionRemote.pairing.searchingForDevices' => 'Söker efter enheter...',
'companionRemote.pairing.tipDesktop' => 'Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny', 'companionRemote.pairing.noDevicesFound' => 'Inga enheter hittades i ditt nätverk',
'companionRemote.pairing.tipScan' => 'Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator', 'companionRemote.pairing.noDevicesHint' => 'Se till att Plezy är öppet på din dator och att båda enheterna är på samma WiFi-nätverk',
'companionRemote.pairing.tipWifi' => 'Se till att båda enheterna är på samma WiFi-nätverk', 'companionRemote.pairing.availableDevices' => 'Tillgängliga enheter',
'companionRemote.pairing.cameraPermissionRequired' => 'Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.', 'companionRemote.pairing.manualConnection' => 'Manuell anslutning',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kunde inte starta kameran: ${error}', 'companionRemote.pairing.cryptoInitFailed' => 'Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.',
'companionRemote.pairing.scanInstruction' => 'Rikta kameran mot QR-koden som visas på din dator', 'companionRemote.pairing.validationHostRequired' => 'Ange värdadress',
'companionRemote.pairing.invalidQrCode' => 'Ogiltigt QR-kodformat',
'companionRemote.pairing.validationHostRequired' => 'Ange en värdadress',
'companionRemote.pairing.validationHostFormat' => 'Format måste vara IP:port (t.ex. 192.168.1.100:48632)', 'companionRemote.pairing.validationHostFormat' => 'Format måste vara IP:port (t.ex. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Ange ett sessions-ID', 'companionRemote.pairing.connectionTimedOut' => 'Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.',
'companionRemote.pairing.validationSessionIdLength' => 'Sessions-ID måste vara 8 tecken', 'companionRemote.pairing.sessionNotFound' => 'Kunde inte hitta enheten. Se till att Plezy körs på värden.',
'companionRemote.pairing.validationPinRequired' => 'Ange en PIN', 'companionRemote.pairing.authFailed' => 'Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.',
'companionRemote.pairing.validationPinLength' => 'PIN måste vara 6 siffror',
'companionRemote.pairing.connectionTimedOut' => 'Anslutningen tog för lång tid. Kontrollera sessions-ID och PIN.',
'companionRemote.pairing.sessionNotFound' => 'Kunde inte hitta sessionen. Kontrollera dina uppgifter.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunde inte ansluta: ${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunde inte ansluta: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vill du koppla från fjärrsessionen?', 'companionRemote.remote.disconnectConfirm' => 'Vill du koppla från fjärrsessionen?',
'companionRemote.remote.reconnecting' => 'Återansluter...', 'companionRemote.remote.reconnecting' => 'Återansluter...',
+48 -68
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsZh implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration'; @override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing'; @override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior'; @override String get behavior => 'Behavior';
@override String get companionRemoteServer => '配套遥控服务器';
@override String get companionRemoteServerDescription => '允许网络上的移动设备控制此应用';
} }
// Path: search // Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionZh implements TranslationsCompanionRemo
final TranslationsZh _root; // ignore: unused_field final TranslationsZh _root; // ignore: unused_field
// Translations // Translations
@override String get creatingSession => '正在创建远程会话...'; @override String get startingServer => '正在启动远程服务器...';
@override String get failedToCreate => '创建远程会话失败:'; @override String get failedToCreate => '启动远程服务器失败:';
@override String get noSession => '没有可用的会话';
@override String get scanQrCode => '扫描 QR 码';
@override String get orEnterManually => '或手动输入';
@override String get hostAddress => '主机地址'; @override String get hostAddress => '主机地址';
@override String get sessionId => '会话 ID';
@override String get pin => 'PIN';
@override String get connected => '已连接'; @override String get connected => '已连接';
@override String get waitingForConnection => '等待连接中...'; @override String get serverRunning => '远程服务器已启动';
@override String get serverStopped => '远程服务器已停止';
@override String get serverRunningDescription => '网络上的移动设备可以发现并连接到此应用';
@override String get serverStoppedDescription => '启动服务器以允许移动设备连接';
@override String get usePhoneToControl => '使用移动设备控制此应用'; @override String get usePhoneToControl => '使用移动设备控制此应用';
@override String copiedToClipboard({required Object label}) => '${label}已复制到剪贴板'; @override String get startServer => '启动服务器';
@override String get copyToClipboard => '复制到剪贴板'; @override String get stopServer => '停止服务器';
@override String get newSession => '新建会话';
@override String get minimize => '最小化'; @override String get minimize => '最小化';
} }
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingZh implements TranslationsCompanionRemo
final TranslationsZh _root; // ignore: unused_field final TranslationsZh _root; // ignore: unused_field
// Translations // Translations
@override String get scan => '扫描'; @override String get pairWithDesktop => '连接到桌面';
@override String get manual => '手动'; @override String get discoveryDescription => '网络上使用相同Plex账户运行Plezy的设备将自动显示';
@override String get pairWithDesktop => '与桌面配对';
@override String get enterSessionDetails => '输入桌面设备上显示的会话信息';
@override String get hostAddressHint => '192.168.1.100:48632'; @override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => '输入8位会话 ID'; @override String get connecting => '正在连接...';
@override String get pinHint => '输入6位 PIN'; @override String get searchingForDevices => '正在搜索设备...';
@override String get connecting => '连接中...'; @override String get noDevicesFound => '未在网络上找到设备';
@override String get tips => '提示'; @override String get noDevicesHint => '请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上';
@override String get tipDesktop => '在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote'; @override String get availableDevices => '可用设备';
@override String get tipScan => '使用扫描选项卡扫描桌面上的 QR 码以快速配对'; @override String get manualConnection => '手动连接';
@override String get tipWifi => '请确保两台设备连接到同一个 WiFi 网络'; @override String get cryptoInitFailed => '无法初始化安全连接。请确保已登录Plex账户。';
@override String get cameraPermissionRequired => '扫描 QR 码需要相机权限。\n请在设备设置中授予相机访问权限。';
@override String cameraError({required Object error}) => '无法启动相机:${error}';
@override String get scanInstruction => '将相机对准桌面上显示的 QR 码';
@override String get invalidQrCode => '无效的 QR 码格式';
@override String get validationHostRequired => '请输入主机地址'; @override String get validationHostRequired => '请输入主机地址';
@override String get validationHostFormat => '格式必须为 IP:端口(例如 192.168.1.100:48632'; @override String get validationHostFormat => '格式必须为IP:端口(例如 192.168.1.100:48632';
@override String get validationSessionIdRequired => '请输入会话 ID'; @override String get connectionTimedOut => '连接超时。请确保两台设备在同一网络上。';
@override String get validationSessionIdLength => '会话 ID 必须为8个字符'; @override String get sessionNotFound => '未找到设备。请确保Plezy正在主机上运行。';
@override String get validationPinRequired => '请输入 PIN'; @override String get authFailed => '认证失败。请确保两台设备使用相同的Plex账户。';
@override String get validationPinLength => 'PIN 必须为6位数字';
@override String get connectionTimedOut => '连接超时。请检查会话 ID 和 PIN。';
@override String get sessionNotFound => '找不到会话。请检查您的凭据。';
@override String failedToConnect({required Object error}) => '连接失败:${error}'; @override String failedToConnect({required Object error}) => '连接失败:${error}';
} }
@@ -1600,6 +1590,8 @@ extension on TranslationsZh {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration', 'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing', 'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior', 'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => '配套遥控服务器',
'settings.companionRemoteServerDescription' => '允许网络上的移动设备控制此应用',
'search.hint' => '搜索电影、系列、音乐...', 'search.hint' => '搜索电影、系列、音乐...',
'search.tryDifferentTerm' => '尝试不同的搜索词', 'search.tryDifferentTerm' => '尝试不同的搜索词',
'search.searchYourMedia' => '搜索媒体', 'search.searchYourMedia' => '搜索媒体',
@@ -1881,10 +1873,10 @@ extension on TranslationsZh {
'libraries.groupings.seasons' => '', 'libraries.groupings.seasons' => '',
'libraries.groupings.episodes' => '', 'libraries.groupings.episodes' => '',
'libraries.groupings.folders' => '文件夹', 'libraries.groupings.folders' => '文件夹',
'about.title' => '关于',
'about.openSourceLicenses' => '开源许可证',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'about.title' => '关于',
'about.openSourceLicenses' => '开源许可证',
'about.versionLabel' => ({required Object version}) => '版本 ${version}', 'about.versionLabel' => ({required Object version}) => '版本 ${version}',
'about.appDescription' => '一款精美的 Flutter Plex 客户端', 'about.appDescription' => '一款精美的 Flutter Plex 客户端',
'about.viewLicensesDescription' => '查看第三方库的许可证', 'about.viewLicensesDescription' => '查看第三方库的许可证',
@@ -2076,45 +2068,33 @@ extension on TranslationsZh {
'companionRemote.remoteControl' => '远程控制', 'companionRemote.remoteControl' => '远程控制',
'companionRemote.controlDesktop' => '控制桌面设备', 'companionRemote.controlDesktop' => '控制桌面设备',
'companionRemote.connectedTo' => ({required Object name}) => '已连接到 ${name}', 'companionRemote.connectedTo' => ({required Object name}) => '已连接到 ${name}',
'companionRemote.session.creatingSession' => '正在创建远程会话...', 'companionRemote.session.startingServer' => '正在启动远程服务器...',
'companionRemote.session.failedToCreate' => '创建远程会话失败:', 'companionRemote.session.failedToCreate' => '启动远程服务器失败:',
'companionRemote.session.noSession' => '没有可用的会话',
'companionRemote.session.scanQrCode' => '扫描 QR 码',
'companionRemote.session.orEnterManually' => '或手动输入',
'companionRemote.session.hostAddress' => '主机地址', 'companionRemote.session.hostAddress' => '主机地址',
'companionRemote.session.sessionId' => '会话 ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => '已连接', 'companionRemote.session.connected' => '已连接',
'companionRemote.session.waitingForConnection' => '等待连接中...', 'companionRemote.session.serverRunning' => '远程服务器已启动',
'companionRemote.session.serverStopped' => '远程服务器已停止',
'companionRemote.session.serverRunningDescription' => '网络上的移动设备可以发现并连接到此应用',
'companionRemote.session.serverStoppedDescription' => '启动服务器以允许移动设备连接',
'companionRemote.session.usePhoneToControl' => '使用移动设备控制此应用', 'companionRemote.session.usePhoneToControl' => '使用移动设备控制此应用',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label}已复制到剪贴板', 'companionRemote.session.startServer' => '启动服务器',
'companionRemote.session.copyToClipboard' => '复制到剪贴板', 'companionRemote.session.stopServer' => '停止服务器',
'companionRemote.session.newSession' => '新建会话',
'companionRemote.session.minimize' => '最小化', 'companionRemote.session.minimize' => '最小化',
'companionRemote.pairing.scan' => '扫描', 'companionRemote.pairing.pairWithDesktop' => '连接到桌面',
'companionRemote.pairing.manual' => '手动', 'companionRemote.pairing.discoveryDescription' => '网络上使用相同Plex账户运行Plezy的设备将自动显示',
'companionRemote.pairing.pairWithDesktop' => '与桌面配对',
'companionRemote.pairing.enterSessionDetails' => '输入桌面设备上显示的会话信息',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632', 'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '输入8位会话 ID', 'companionRemote.pairing.connecting' => '正在连接...',
'companionRemote.pairing.pinHint' => '输入6位 PIN', 'companionRemote.pairing.searchingForDevices' => '正在搜索设备...',
'companionRemote.pairing.connecting' => '连接中...', 'companionRemote.pairing.noDevicesFound' => '未在网络上找到设备',
'companionRemote.pairing.tips' => '提示', 'companionRemote.pairing.noDevicesHint' => '请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上',
'companionRemote.pairing.tipDesktop' => '在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote', 'companionRemote.pairing.availableDevices' => '可用设备',
'companionRemote.pairing.tipScan' => '使用扫描选项卡扫描桌面上的 QR 码以快速配对', 'companionRemote.pairing.manualConnection' => '手动连接',
'companionRemote.pairing.tipWifi' => '请确保两台设备连接到同一个 WiFi 网络', 'companionRemote.pairing.cryptoInitFailed' => '无法初始化安全连接。请确保已登录Plex账户。',
'companionRemote.pairing.cameraPermissionRequired' => '扫描 QR 码需要相机权限。\n请在设备设置中授予相机访问权限。',
'companionRemote.pairing.cameraError' => ({required Object error}) => '无法启动相机:${error}',
'companionRemote.pairing.scanInstruction' => '将相机对准桌面上显示的 QR 码',
'companionRemote.pairing.invalidQrCode' => '无效的 QR 码格式',
'companionRemote.pairing.validationHostRequired' => '请输入主机地址', 'companionRemote.pairing.validationHostRequired' => '请输入主机地址',
'companionRemote.pairing.validationHostFormat' => '格式必须为 IP:端口(例如 192.168.1.100:48632', 'companionRemote.pairing.validationHostFormat' => '格式必须为IP:端口(例如 192.168.1.100:48632',
'companionRemote.pairing.validationSessionIdRequired' => '请输入会话 ID', 'companionRemote.pairing.connectionTimedOut' => '连接超时。请确保两台设备在同一网络上。',
'companionRemote.pairing.validationSessionIdLength' => '会话 ID 必须为8个字符', 'companionRemote.pairing.sessionNotFound' => '未找到设备。请确保Plezy正在主机上运行。',
'companionRemote.pairing.validationPinRequired' => '请输入 PIN', 'companionRemote.pairing.authFailed' => '认证失败。请确保两台设备使用相同的Plex账户。',
'companionRemote.pairing.validationPinLength' => 'PIN 必须为6位数字',
'companionRemote.pairing.connectionTimedOut' => '连接超时。请检查会话 ID 和 PIN。',
'companionRemote.pairing.sessionNotFound' => '找不到会话。请检查您的凭据。',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => '连接失败:${error}', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => '连接失败:${error}',
'companionRemote.remote.disconnectConfirm' => '是否要断开远程会话的连接?', 'companionRemote.remote.disconnectConfirm' => '是否要断开远程会话的连接?',
'companionRemote.remote.reconnecting' => '重新连接中...', 'companionRemote.remote.reconnecting' => '重新连接中...',
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "Companion Remote-server",
"companionRemoteServerDescription": "Tillåt mobila enheter i ditt nätverk att styra denna app"
}, },
"search": { "search": {
"hint": "Sök filmer, serier, musik...", "hint": "Sök filmer, serier, musik...",
@@ -778,47 +780,35 @@
"controlDesktop": "Styr en datorenhet", "controlDesktop": "Styr en datorenhet",
"connectedTo": "Ansluten till ${name}", "connectedTo": "Ansluten till ${name}",
"session": { "session": {
"creatingSession": "Skapar fjärrsession...", "startingServer": "Startar fjärrserver...",
"failedToCreate": "Kunde inte skapa fjärrsession:", "failedToCreate": "Kunde inte starta fjärrserver:",
"noSession": "Ingen session tillgänglig",
"scanQrCode": "Skanna QR-kod",
"orEnterManually": "Eller ange manuellt",
"hostAddress": "Värdadress", "hostAddress": "Värdadress",
"sessionId": "Sessions-ID",
"pin": "PIN",
"connected": "Ansluten", "connected": "Ansluten",
"waitingForConnection": "Väntar på anslutning...", "serverRunning": "Fjärrserver aktiv",
"serverStopped": "Fjärrserver stoppad",
"serverRunningDescription": "Mobila enheter i ditt nätverk kan upptäcka och ansluta till denna app",
"serverStoppedDescription": "Starta servern för att tillåta mobila enheter att ansluta",
"usePhoneToControl": "Använd din mobila enhet för att styra denna app", "usePhoneToControl": "Använd din mobila enhet för att styra denna app",
"copiedToClipboard": "${label} kopierad till urklipp", "startServer": "Starta server",
"copyToClipboard": "Kopiera till urklipp", "stopServer": "Stoppa server",
"newSession": "Ny session",
"minimize": "Minimera" "minimize": "Minimera"
}, },
"pairing": { "pairing": {
"scan": "Skanna", "pairWithDesktop": "Anslut till dator",
"manual": "Manuell", "discoveryDescription": "Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt",
"pairWithDesktop": "Parkoppla med dator",
"enterSessionDetails": "Ange sessionsuppgifterna som visas på din datorenhet",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Ange 8-teckens sessions-ID",
"pinHint": "Ange 6-siffrig PIN",
"connecting": "Ansluter...", "connecting": "Ansluter...",
"tips": "Tips", "searchingForDevices": "Söker efter enheter...",
"tipDesktop": "Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny", "noDevicesFound": "Inga enheter hittades i ditt nätverk",
"tipScan": "Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator", "noDevicesHint": "Se till att Plezy är öppet på din dator och att båda enheterna är på samma WiFi-nätverk",
"tipWifi": "Se till att båda enheterna är på samma WiFi-nätverk", "availableDevices": "Tillgängliga enheter",
"cameraPermissionRequired": "Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.", "manualConnection": "Manuell anslutning",
"cameraError": "Kunde inte starta kameran: ${error}", "cryptoInitFailed": "Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.",
"scanInstruction": "Rikta kameran mot QR-koden som visas på din dator", "validationHostRequired": "Ange värdadress",
"invalidQrCode": "Ogiltigt QR-kodformat",
"validationHostRequired": "Ange en värdadress",
"validationHostFormat": "Format måste vara IP:port (t.ex. 192.168.1.100:48632)", "validationHostFormat": "Format måste vara IP:port (t.ex. 192.168.1.100:48632)",
"validationSessionIdRequired": "Ange ett sessions-ID", "connectionTimedOut": "Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.",
"validationSessionIdLength": "Sessions-ID måste vara 8 tecken", "sessionNotFound": "Kunde inte hitta enheten. Se till att Plezy körs på värden.",
"validationPinRequired": "Ange en PIN", "authFailed": "Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.",
"validationPinLength": "PIN måste vara 6 siffror",
"connectionTimedOut": "Anslutningen tog för lång tid. Kontrollera sessions-ID och PIN.",
"sessionNotFound": "Kunde inte hitta sessionen. Kontrollera dina uppgifter.",
"failedToConnect": "Kunde inte ansluta: ${error}" "failedToConnect": "Kunde inte ansluta: ${error}"
}, },
"remote": { "remote": {
+24 -34
View File
@@ -238,7 +238,9 @@
"player": "Player", "player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration", "subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing", "seekAndTiming": "Seek & Timing",
"behavior": "Behavior" "behavior": "Behavior",
"companionRemoteServer": "配套遥控服务器",
"companionRemoteServerDescription": "允许网络上的移动设备控制此应用"
}, },
"search": { "search": {
"hint": "搜索电影、系列、音乐...", "hint": "搜索电影、系列、音乐...",
@@ -778,47 +780,35 @@
"controlDesktop": "控制桌面设备", "controlDesktop": "控制桌面设备",
"connectedTo": "已连接到 ${name}", "connectedTo": "已连接到 ${name}",
"session": { "session": {
"creatingSession": "正在创建远程会话...", "startingServer": "正在启动远程服务器...",
"failedToCreate": "创建远程会话失败:", "failedToCreate": "启动远程服务器失败:",
"noSession": "没有可用的会话",
"scanQrCode": "扫描 QR 码",
"orEnterManually": "或手动输入",
"hostAddress": "主机地址", "hostAddress": "主机地址",
"sessionId": "会话 ID",
"pin": "PIN",
"connected": "已连接", "connected": "已连接",
"waitingForConnection": "等待连接中...", "serverRunning": "远程服务器已启动",
"serverStopped": "远程服务器已停止",
"serverRunningDescription": "网络上的移动设备可以发现并连接到此应用",
"serverStoppedDescription": "启动服务器以允许移动设备连接",
"usePhoneToControl": "使用移动设备控制此应用", "usePhoneToControl": "使用移动设备控制此应用",
"copiedToClipboard": "${label}已复制到剪贴板", "startServer": "启动服务器",
"copyToClipboard": "复制到剪贴板", "stopServer": "停止服务器",
"newSession": "新建会话",
"minimize": "最小化" "minimize": "最小化"
}, },
"pairing": { "pairing": {
"scan": "扫描", "pairWithDesktop": "连接到桌面",
"manual": "手动", "discoveryDescription": "网络上使用相同Plex账户运行Plezy的设备将自动显示",
"pairWithDesktop": "与桌面配对",
"enterSessionDetails": "输入桌面设备上显示的会话信息",
"hostAddressHint": "192.168.1.100:48632", "hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "输入8位会话 ID", "connecting": "正在连接...",
"pinHint": "输入6位 PIN", "searchingForDevices": "正在搜索设备...",
"connecting": "连接中...", "noDevicesFound": "未在网络上找到设备",
"tips": "提示", "noDevicesHint": "请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上",
"tipDesktop": "在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote", "availableDevices": "可用设备",
"tipScan": "使用扫描选项卡扫描桌面上的 QR 码以快速配对", "manualConnection": "手动连接",
"tipWifi": "请确保两台设备连接到同一个 WiFi 网络", "cryptoInitFailed": "无法初始化安全连接。请确保已登录Plex账户。",
"cameraPermissionRequired": "扫描 QR 码需要相机权限。\n请在设备设置中授予相机访问权限。",
"cameraError": "无法启动相机:${error}",
"scanInstruction": "将相机对准桌面上显示的 QR 码",
"invalidQrCode": "无效的 QR 码格式",
"validationHostRequired": "请输入主机地址", "validationHostRequired": "请输入主机地址",
"validationHostFormat": "格式必须为 IP:端口(例如 192.168.1.100:48632", "validationHostFormat": "格式必须为IP:端口(例如 192.168.1.100:48632",
"validationSessionIdRequired": "请输入会话 ID", "connectionTimedOut": "连接超时。请确保两台设备在同一网络上。",
"validationSessionIdLength": "会话 ID 必须为8个字符", "sessionNotFound": "未找到设备。请确保Plezy正在主机上运行。",
"validationPinRequired": "请输入 PIN", "authFailed": "认证失败。请确保两台设备使用相同的Plex账户。",
"validationPinLength": "PIN 必须为6位数字",
"connectionTimedOut": "连接超时。请检查会话 ID 和 PIN。",
"sessionNotFound": "找不到会话。请检查您的凭据。",
"failedToConnect": "连接失败:${error}" "failedToConnect": "连接失败:${error}"
}, },
"remote": { "remote": {
@@ -56,8 +56,6 @@ class RemoteDevice {
@JsonSerializable() @JsonSerializable()
class RemoteSession { class RemoteSession {
final String sessionId;
final String pin;
@JsonKey(unknownEnumValue: RemoteSessionRole.remote) @JsonKey(unknownEnumValue: RemoteSessionRole.remote)
final RemoteSessionRole role; final RemoteSessionRole role;
@JsonKey(unknownEnumValue: RemoteSessionStatus.disconnected) @JsonKey(unknownEnumValue: RemoteSessionStatus.disconnected)
@@ -67,8 +65,6 @@ class RemoteSession {
final String? errorMessage; final String? errorMessage;
RemoteSession({ RemoteSession({
required this.sessionId,
required this.pin,
required this.role, required this.role,
this.status = RemoteSessionStatus.disconnected, this.status = RemoteSessionStatus.disconnected,
this.connectedDevice, this.connectedDevice,
@@ -85,8 +81,6 @@ class RemoteSession {
Map<String, dynamic> toJson() => _$RemoteSessionToJson(this); Map<String, dynamic> toJson() => _$RemoteSessionToJson(this);
RemoteSession copyWith({ RemoteSession copyWith({
String? sessionId,
String? pin,
RemoteSessionRole? role, RemoteSessionRole? role,
RemoteSessionStatus? status, RemoteSessionStatus? status,
RemoteDevice? connectedDevice, RemoteDevice? connectedDevice,
@@ -96,8 +90,6 @@ class RemoteSession {
bool clearErrorMessage = false, bool clearErrorMessage = false,
}) { }) {
return RemoteSession( return RemoteSession(
sessionId: sessionId ?? this.sessionId,
pin: pin ?? this.pin,
role: role ?? this.role, role: role ?? this.role,
status: status ?? this.status, status: status ?? this.status,
connectedDevice: clearConnectedDevice ? null : (connectedDevice ?? this.connectedDevice), connectedDevice: clearConnectedDevice ? null : (connectedDevice ?? this.connectedDevice),
@@ -29,8 +29,6 @@ Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) =>
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) => RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
RemoteSession( RemoteSession(
sessionId: json['sessionId'] as String,
pin: json['pin'] as String,
role: $enumDecode( role: $enumDecode(
_$RemoteSessionRoleEnumMap, _$RemoteSessionRoleEnumMap,
json['role'], json['role'],
@@ -56,8 +54,6 @@ RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) => Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) =>
<String, dynamic>{ <String, dynamic>{
'sessionId': instance.sessionId,
'pin': instance.pin,
'role': _$RemoteSessionRoleEnumMap[instance.role]!, 'role': _$RemoteSessionRoleEnumMap[instance.role]!,
'status': _$RemoteSessionStatusEnumMap[instance.status]!, 'status': _$RemoteSessionStatusEnumMap[instance.status]!,
'connectedDevice': instance.connectedDevice, 'connectedDevice': instance.connectedDevice,
+255 -103
View File
@@ -6,14 +6,21 @@ import 'package:device_info_plus/device_info_plus.dart';
import '../models/companion_remote/remote_command.dart'; import '../models/companion_remote/remote_command.dart';
import '../models/companion_remote/remote_session.dart'; import '../models/companion_remote/remote_session.dart';
import '../models/plex_home.dart';
import '../services/companion_remote/companion_remote_peer_service.dart'; import '../services/companion_remote/companion_remote_peer_service.dart';
import '../services/companion_remote/lan_discovery_service.dart';
import '../services/companion_remote/remote_auth_service.dart';
import '../services/storage_service.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
export '../services/companion_remote/lan_discovery_service.dart' show DiscoveredHost;
typedef CommandReceivedCallback = void Function(RemoteCommand command); typedef CommandReceivedCallback = void Function(RemoteCommand command);
class CompanionRemoteProvider with ChangeNotifier { class CompanionRemoteProvider with ChangeNotifier {
RemoteSession? _session; RemoteSession? _session;
CompanionRemotePeerService? _peerService; CompanionRemotePeerService? _peerService;
LanDiscoveryService? _discoveryService;
String _deviceName = 'Unknown Device'; String _deviceName = 'Unknown Device';
String _platform = 'unknown'; String _platform = 'unknown';
bool _isPlayerActive = false; bool _isPlayerActive = false;
@@ -23,9 +30,17 @@ class CompanionRemoteProvider with ChangeNotifier {
Timer? _reconnectTimer; Timer? _reconnectTimer;
int _reconnectAttempts = 0; int _reconnectAttempts = 0;
bool _intentionalDisconnect = false; bool _intentionalDisconnect = false;
String? _lastSessionId;
String? _lastPin; // Reconnection context (only hostAddresses and hostClientId are connection-specific)
String? _lastHostAddress; List<String>? _lastHostAddresses;
String? _lastHostClientId;
// Crypto context (derived in memory, never persisted)
List<int>? _homeSecret;
List<int>? _discoveryKey;
String? _clientIdentifier;
String? _userUUID;
List<String>? _homeUserUUIDs;
int get reconnectAttempts => _reconnectAttempts; int get reconnectAttempts => _reconnectAttempts;
@@ -43,10 +58,9 @@ class CompanionRemoteProvider with ChangeNotifier {
bool get isConnected => _session?.isConnected ?? false; bool get isConnected => _session?.isConnected ?? false;
RemoteSession? get session => _session; RemoteSession? get session => _session;
RemoteSessionStatus get status => _session?.status ?? RemoteSessionStatus.disconnected; RemoteSessionStatus get status => _session?.status ?? RemoteSessionStatus.disconnected;
String? get sessionId => _session?.sessionId;
String? get pin => _session?.pin;
RemoteDevice? get connectedDevice => _session?.connectedDevice; RemoteDevice? get connectedDevice => _session?.connectedDevice;
bool get isPlayerActive => _isPlayerActive; bool get isPlayerActive => _isPlayerActive;
bool get isHostServerRunning => _peerService?.isServerRunning ?? false;
CompanionRemoteProvider() { CompanionRemoteProvider() {
_initializeDeviceInfo(); _initializeDeviceInfo();
@@ -86,6 +100,219 @@ class CompanionRemoteProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Initialize crypto context from Plex home data.
/// Must be called before startHostServer or connectToDiscoveredHost.
Future<bool> initializeCrypto(PlexHome? home, StorageService storage) async {
if (home == null || home.adminUser == null) {
appLogger.w('CompanionRemote: Cannot init crypto — no home data');
return false;
}
try {
final auth = RemoteAuthService.instance;
_homeSecret = await auth.deriveHomeSecretFromHome(home);
_discoveryKey = await auth.deriveDiscoveryKey(_homeSecret!);
_clientIdentifier = storage.getClientIdentifier();
_userUUID = storage.getCurrentUserUUID();
_homeUserUUIDs = home.users.map((u) => u.uuid).toList();
appLogger.d('CompanionRemote: Crypto context initialized');
return true;
} catch (e) {
appLogger.e('CompanionRemote: Failed to init crypto', error: e);
return false;
}
}
bool get isCryptoReady => _homeSecret != null && _discoveryKey != null && _clientIdentifier != null;
/// Convenience: ensure crypto is initialized using providers from context.
/// Returns true if crypto is ready (already initialized or just initialized).
Future<bool> ensureCryptoReady(PlexHome? home) async {
if (isCryptoReady) return true;
final storage = await StorageService.getInstance();
return initializeCrypto(home, storage);
}
// ── Host Server ──
/// Start the host server and begin LAN broadcasting. Idempotent.
Future<void> startHostServer() async {
if (_peerService?.isServerRunning == true) return;
if (!isCryptoReady) {
appLogger.w('CompanionRemote: Cannot start host — crypto not initialized');
return;
}
appLogger.d('CompanionRemote: Starting host server');
_peerService ??= CompanionRemotePeerService();
_setupPeerServiceListeners();
try {
final result = await _peerService!.createSession(
_deviceName,
_platform,
_homeSecret!,
_clientIdentifier!,
_homeUserUUIDs!,
);
_session = RemoteSession(
role: RemoteSessionRole.host,
status: RemoteSessionStatus.connected,
);
notifyListeners();
// Start LAN discovery broadcasting
_discoveryService ??= LanDiscoveryService();
final localIps = result.addresses.map((a) => a.split(':').first).toList();
await _discoveryService!.startBroadcasting(
discoveryKey: _discoveryKey!,
deviceName: _deviceName,
platform: _platform,
clientId: _clientIdentifier!,
wsPort: result.port,
ips: localIps,
);
appLogger.d('CompanionRemote: Host server running, broadcasting on LAN');
} catch (e) {
appLogger.e('CompanionRemote: Failed to start host server', error: e);
_session = RemoteSession(
role: RemoteSessionRole.host,
status: RemoteSessionStatus.error,
errorMessage: e.toString(),
);
notifyListeners();
}
}
/// Stop the host server and LAN broadcasting.
Future<void> stopHostServer() async {
_intentionalDisconnect = true;
_discoveryService?.stopBroadcasting();
if (_peerService != null) {
await _peerService!.disconnect();
_peerService = null;
}
_cleanupSubscriptions();
_session = null;
_isPlayerActive = false;
_intentionalDisconnect = false;
notifyListeners();
}
// ── Client: Discovery ──
/// Start listening for host beacons. Returns a stream of discovered hosts.
Stream<List<DiscoveredHost>>? discoverHosts() {
if (!isCryptoReady) {
appLogger.w('CompanionRemote: Cannot discover — crypto not initialized');
return null;
}
_discoveryService ??= LanDiscoveryService();
return _discoveryService!.startListening(discoveryKey: _discoveryKey!);
}
/// Stop listening for host beacons.
void stopDiscovery() {
_discoveryService?.stopListening();
}
/// Connect to a discovered host as a remote client.
Future<void> connectToDiscoveredHost(DiscoveredHost host) async {
if (!isCryptoReady) {
throw StateError('Crypto not initialized');
}
await leaveSession();
_lastHostAddresses = host.addresses;
_lastHostClientId = host.clientId;
appLogger.d('CompanionRemote: Connecting to ${host.name} at ${host.addresses}');
_peerService = CompanionRemotePeerService();
_setupPeerServiceListeners();
_session = RemoteSession(
role: RemoteSessionRole.remote,
status: RemoteSessionStatus.connecting,
);
notifyListeners();
try {
final winner = await _peerService!.joinSessionRacing(
_deviceName,
_platform,
host.addresses,
_homeSecret!,
host.clientId,
_userUUID!,
_clientIdentifier!,
);
_lastHostAddresses = [winner];
_session = _session?.copyWith(status: RemoteSessionStatus.connected);
notifyListeners();
appLogger.d('CompanionRemote: Connected to ${host.name} via $winner');
} catch (e) {
appLogger.e('CompanionRemote: Failed to connect to host', error: e);
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString());
notifyListeners();
rethrow;
}
}
/// Connect to a host by manual IP:port entry.
Future<void> connectToManualHost(String hostAddress) async {
if (!isCryptoReady) {
throw StateError('Crypto not initialized');
}
await leaveSession();
_lastHostAddresses = [hostAddress];
_lastHostClientId = '';
appLogger.d('CompanionRemote: Connecting to manual host $hostAddress');
_peerService = CompanionRemotePeerService();
_setupPeerServiceListeners();
_session = RemoteSession(
role: RemoteSessionRole.remote,
status: RemoteSessionStatus.connecting,
);
notifyListeners();
try {
await _peerService!.joinSession(
_deviceName,
_platform,
hostAddress,
_homeSecret!,
'', // Empty hostClientId — accept any host in same home
_userUUID!,
_clientIdentifier!,
);
_session = _session?.copyWith(status: RemoteSessionStatus.connected);
notifyListeners();
} catch (e) {
appLogger.e('CompanionRemote: Failed to connect to manual host', error: e);
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString());
notifyListeners();
rethrow;
}
}
// ── Peer service listeners ──
void _setupPeerServiceListeners() { void _setupPeerServiceListeners() {
_commandSubscription = _peerService!.onCommandReceived.listen( _commandSubscription = _peerService!.onCommandReceived.listen(
(command) { (command) {
@@ -118,7 +345,6 @@ class CompanionRemoteProvider with ChangeNotifier {
_session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true); _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true);
notifyListeners(); notifyListeners();
} else if (isHost) { } else if (isHost) {
// Host keeps the server running — the client will reconnect on its own
_session = _session?.copyWith( _session = _session?.copyWith(
status: RemoteSessionStatus.reconnecting, status: RemoteSessionStatus.reconnecting,
clearConnectedDevice: true, clearConnectedDevice: true,
@@ -183,93 +409,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_statusSubscription = null; _statusSubscription = null;
} }
Future<({String sessionId, String pin, List<String> addresses})> createSession() async { // ── Commands ──
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}, Addresses: ${result.addresses}',
);
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<void> joinSession(String sessionId, String pin, String hostAddress) async {
await joinSessionMulti(sessionId, pin, [hostAddress]);
}
Future<void> joinSessionMulti(String sessionId, String pin, List<String> hostAddresses) async {
if (hostAddresses.isEmpty) {
throw ArgumentError('hostAddresses must not be empty');
}
await leaveSession();
_lastSessionId = sessionId;
_lastPin = pin;
// Store first address as fallback for reconnection; will be updated with winner
_lastHostAddress = hostAddresses.first;
appLogger.d('CompanionRemote: Joining session - ID: $sessionId, Hosts: $hostAddresses');
_peerService = CompanionRemotePeerService();
_setupPeerServiceListeners();
_session = RemoteSession(
sessionId: sessionId,
pin: pin,
role: RemoteSessionRole.remote,
status: RemoteSessionStatus.connecting,
);
notifyListeners();
try {
final winner = await _peerService!.joinSessionRacing(
sessionId,
pin,
_deviceName,
_platform,
hostAddresses,
);
_lastHostAddress = winner;
_session = _session?.copyWith(status: RemoteSessionStatus.connected);
notifyListeners();
appLogger.d('CompanionRemote: Successfully joined session via $winner');
} 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<String, dynamic>? data}) { void sendCommand(RemoteCommandType type, {Map<String, dynamic>? data}) {
if (_peerService == null || !isConnected) { if (_peerService == null || !isConnected) {
@@ -281,6 +421,8 @@ class CompanionRemoteProvider with ChangeNotifier {
_peerService!.sendCommand(RemoteCommand(type: type, data: data)); _peerService!.sendCommand(RemoteCommand(type: type, data: data));
} }
// ── Reconnection ──
void _scheduleReconnect() { void _scheduleReconnect() {
if (_reconnectAttempts >= _maxReconnectAttempts) { if (_reconnectAttempts >= _maxReconnectAttempts) {
appLogger.w('CompanionRemote: Max reconnect attempts reached'); appLogger.w('CompanionRemote: Max reconnect attempts reached');
@@ -293,7 +435,7 @@ class CompanionRemoteProvider with ChangeNotifier {
return; return;
} }
final delay = Duration(seconds: 1 << _reconnectAttempts); // 1s, 2s, 4s, 8s, 16s final delay = Duration(seconds: 1 << _reconnectAttempts);
_reconnectAttempts++; _reconnectAttempts++;
appLogger.d('CompanionRemote: Reconnect attempt $_reconnectAttempts in ${delay.inSeconds}s'); appLogger.d('CompanionRemote: Reconnect attempt $_reconnectAttempts in ${delay.inSeconds}s');
@@ -302,8 +444,8 @@ class CompanionRemoteProvider with ChangeNotifier {
} }
Future<void> _attemptReconnect() async { Future<void> _attemptReconnect() async {
if (_lastSessionId == null || _lastPin == null || _lastHostAddress == null) { if (_lastHostAddresses == null || !isCryptoReady) {
appLogger.w('CompanionRemote: No stored credentials for reconnect'); appLogger.w('CompanionRemote: No stored context for reconnect');
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost'); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost');
notifyListeners(); notifyListeners();
return; return;
@@ -311,7 +453,6 @@ class CompanionRemoteProvider with ChangeNotifier {
try { try {
appLogger.d('CompanionRemote: Attempting reconnect...'); appLogger.d('CompanionRemote: Attempting reconnect...');
// Clean up old peer service without triggering intentional disconnect
_cleanupSubscriptions(); _cleanupSubscriptions();
try { try {
await _peerService?.disconnect(); await _peerService?.disconnect();
@@ -320,7 +461,15 @@ class CompanionRemoteProvider with ChangeNotifier {
_setupPeerServiceListeners(); _setupPeerServiceListeners();
} }
await _peerService!.joinSession(_lastSessionId!, _lastPin!, _deviceName, _platform, _lastHostAddress!); await _peerService!.joinSession(
_deviceName,
_platform,
_lastHostAddresses!.first,
_homeSecret!,
_lastHostClientId ?? '',
_userUUID!,
_clientIdentifier!,
);
_session = _session?.copyWith(status: RemoteSessionStatus.connected, clearErrorMessage: true); _session = _session?.copyWith(status: RemoteSessionStatus.connected, clearErrorMessage: true);
_reconnectAttempts = 0; _reconnectAttempts = 0;
@@ -334,14 +483,12 @@ class CompanionRemoteProvider with ChangeNotifier {
} }
} }
/// Immediately retry reconnection, skipping the backoff wait
void retryReconnectNow() { void retryReconnectNow() {
_reconnectTimer?.cancel(); _reconnectTimer?.cancel();
_reconnectAttempts = 0; _reconnectAttempts = 0;
_attemptReconnect(); _attemptReconnect();
} }
/// Cancel ongoing reconnection attempts
void cancelReconnect() { void cancelReconnect() {
_reconnectTimer?.cancel(); _reconnectTimer?.cancel();
_reconnectAttempts = 0; _reconnectAttempts = 0;
@@ -354,7 +501,8 @@ class CompanionRemoteProvider with ChangeNotifier {
_reconnectTimer?.cancel(); _reconnectTimer?.cancel();
_reconnectAttempts = 0; _reconnectAttempts = 0;
if (_peerService != null) { // Don't stop the host server when leaving — only stop discovery listening
if (_peerService != null && !isHost) {
appLogger.d('CompanionRemote: Leaving session'); appLogger.d('CompanionRemote: Leaving session');
await _peerService!.disconnect(); await _peerService!.disconnect();
_peerService = null; _peerService = null;
@@ -362,7 +510,9 @@ class CompanionRemoteProvider with ChangeNotifier {
_cleanupSubscriptions(); _cleanupSubscriptions();
_session = null; if (!isHost) {
_session = null;
}
_isPlayerActive = false; _isPlayerActive = false;
_intentionalDisconnect = false; _intentionalDisconnect = false;
notifyListeners(); notifyListeners();
@@ -371,7 +521,9 @@ class CompanionRemoteProvider with ChangeNotifier {
@override @override
void dispose() { void dispose() {
_reconnectTimer?.cancel(); _reconnectTimer?.cancel();
leaveSession(); _discoveryService?.dispose();
_peerService?.dispose();
RemoteAuthService.instance.clearCache();
super.dispose(); super.dispose();
} }
} }
+274 -363
View File
@@ -1,12 +1,11 @@
import 'dart:io'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../providers/companion_remote_provider.dart'; import '../../providers/companion_remote_provider.dart';
import '../../providers/user_profile_provider.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
class PairingScreen extends StatefulWidget { class PairingScreen extends StatefulWidget {
@@ -18,58 +17,97 @@ class PairingScreen extends StatefulWidget {
class _PairingScreenState extends State<PairingScreen> { class _PairingScreenState extends State<PairingScreen> {
final _hostAddressController = TextEditingController(); final _hostAddressController = TextEditingController();
final _sessionIdController = TextEditingController();
final _pinController = TextEditingController();
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
bool _isConnecting = false; bool _isConnecting = false;
String? _errorMessage; String? _errorMessage;
int _selectedTab = 0; bool _showManualEntry = false;
bool _cryptoReady = false;
// QR scanner state StreamSubscription<List<DiscoveredHost>>? _discoverySubscription;
MobileScannerController? _scannerController; List<DiscoveredHost> _hosts = [];
String? _lastScannedCode; bool _isSearching = true;
bool _hasCamera = false; Timer? _searchTimeout;
bool get _canScan => (Platform.isAndroid || Platform.isIOS) && _hasCamera;
// Tab indices: scannable gets Scan (0) + Manual (1), otherwise Manual (0)
int get _scanTabIndex => _canScan ? 0 : -1;
int get _manualTabIndex => _canScan ? 1 : 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_checkCamera(); _initCryptoAndDiscover();
} }
Future<void> _checkCamera() async { Future<void> _initCryptoAndDiscover() async {
if (Platform.isIOS) { final provider = context.read<CompanionRemoteProvider>();
setState(() => _hasCamera = true); final home = context.read<UserProfileProvider>().home;
return; await provider.ensureCryptoReady(home);
} if (!mounted) return;
if (!Platform.isAndroid) return;
try { if (provider.isCryptoReady) {
const channel = MethodChannel('com.plezy/theme'); setState(() => _cryptoReady = true);
final result = await channel.invokeMethod<bool>('hasCamera'); _startDiscovery();
if (mounted) setState(() => _hasCamera = result ?? false); } else {
} catch (e) { setState(() {
debugPrint('Camera check failed: $e'); _cryptoReady = false;
_isSearching = false;
_errorMessage = t.companionRemote.pairing.cryptoInitFailed;
});
} }
} }
void _startDiscovery() {
final provider = context.read<CompanionRemoteProvider>();
final stream = provider.discoverHosts();
if (stream == null) return;
_discoverySubscription = stream.listen((hosts) {
if (mounted) {
setState(() {
_hosts = hosts;
if (hosts.isNotEmpty) _isSearching = false;
});
}
});
// Show "no devices found" after 10 seconds of no results
_searchTimeout = Timer(const Duration(seconds: 10), () {
if (mounted && _hosts.isEmpty) {
setState(() => _isSearching = false);
}
});
}
@override @override
void dispose() { void dispose() {
_hostAddressController.dispose(); _hostAddressController.dispose();
_sessionIdController.dispose(); _discoverySubscription?.cancel();
_pinController.dispose(); _searchTimeout?.cancel();
_scannerController?.dispose(); context.read<CompanionRemoteProvider>().stopDiscovery();
super.dispose(); super.dispose();
} }
Future<void> _connect() async { Future<void> _connectToHost(DiscoveredHost host) async {
if (!_formKey.currentState!.validate()) { setState(() {
return; _isConnecting = true;
_errorMessage = null;
});
try {
final provider = context.read<CompanionRemoteProvider>();
await provider.connectToDiscoveredHost(host);
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
appLogger.e('Failed to connect to host', error: e);
if (!mounted) return;
setState(() {
_isConnecting = false;
_errorMessage = _parseErrorMessage(e.toString());
});
} }
}
Future<void> _connectManual() async {
if (!_formKey.currentState!.validate()) return;
setState(() { setState(() {
_isConnecting = true; _isConnecting = true;
@@ -78,17 +116,13 @@ class _PairingScreenState extends State<PairingScreen> {
try { try {
final provider = context.read<CompanionRemoteProvider>(); final provider = context.read<CompanionRemoteProvider>();
await provider.joinSession( await provider.connectToManualHost(_hostAddressController.text.trim());
_sessionIdController.text.trim().toUpperCase(),
_pinController.text.trim(),
_hostAddressController.text.trim(),
);
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} catch (e) { } catch (e) {
appLogger.e('Failed to join remote session', error: e); appLogger.e('Failed to connect to manual host', error: e);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_isConnecting = false; _isConnecting = false;
@@ -102,73 +136,22 @@ class _PairingScreenState extends State<PairingScreen> {
return t.companionRemote.pairing.connectionTimedOut; return t.companionRemote.pairing.connectionTimedOut;
} else if (error.contains('Failed to connect')) { } else if (error.contains('Failed to connect')) {
return t.companionRemote.pairing.sessionNotFound; return t.companionRemote.pairing.sessionNotFound;
} else if (error.contains('Authentication failed')) {
return t.companionRemote.pairing.authFailed;
} }
return t.companionRemote.pairing.failedToConnect(error: error.replaceAll('Exception: ', '')); return t.companionRemote.pairing.failedToConnect(error: error.replaceAll('Exception: ', ''));
} }
void _handleQrCode(String data) { IconData _platformIcon(String platform) {
// Debounce: don't process the same code twice switch (platform.toLowerCase()) {
if (data == _lastScannedCode) return; case 'macos':
_lastScannedCode = data; return Icons.desktop_mac;
case 'windows':
// Strip URL wrapper if present (e.g. "https://plezy.app/scan#ip1,ip2|port|sid|pin") return Icons.desktop_windows;
final payload = data.contains('#') ? data.split('#').last : data; case 'linux':
final parts = payload.split('|'); return Icons.computer;
if (parts.length == 4) { default:
final ipsField = parts.first; return Icons.devices;
final port = parts[1];
final sessionId = parts[2];
final pin = parts[3];
// Support comma-separated IPs (multi-NIC) or single IP (legacy)
final ips = ipsField.split(',');
final hostAddresses = ips.map((ip) => '$ip:$port').toList();
_scannerController?.stop();
setState(() {
_errorMessage = null;
_isConnecting = true;
});
_connectWithCredentialsMulti(sessionId, pin, hostAddresses);
} else {
setState(() {
_errorMessage = t.companionRemote.pairing.invalidQrCode;
});
}
}
Future<void> _connectWithCredentialsMulti(String sessionId, String pin, List<String> hostAddresses) async {
try {
final provider = context.read<CompanionRemoteProvider>();
await provider.joinSessionMulti(
sessionId.trim().toUpperCase(),
pin.trim(),
hostAddresses.map((a) => a.trim()).toList(),
);
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
appLogger.e('Failed to join remote session', error: e);
if (!mounted) return;
_lastScannedCode = null; // Allow re-scanning
setState(() {
_isConnecting = false;
_errorMessage = _parseErrorMessage(e.toString());
});
_scannerController?.start();
}
}
Future<void> _pasteFromClipboard(TextEditingController controller) async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (!mounted) return;
if (data?.text != null) {
setState(() {
controller.text = data!.text!;
});
} }
} }
@@ -178,290 +161,218 @@ class _PairingScreenState extends State<PairingScreen> {
appBar: AppBar( appBar: AppBar(
title: Text(t.companionRemote.connectToDevice), title: Text(t.companionRemote.connectToDevice),
), ),
body: Column( body: _buildBody(),
);
}
Widget _buildBody() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (_canScan) Icon(Icons.devices, size: 64, color: Theme.of(context).colorScheme.primary),
SegmentedButton<int>( const SizedBox(height: 16),
segments: [ Text(
ButtonSegment( t.companionRemote.pairing.pairWithDesktop,
value: _scanTabIndex, style: Theme.of(context).textTheme.headlineMedium,
label: Text(t.companionRemote.pairing.scan), textAlign: TextAlign.center,
icon: const Icon(Icons.qr_code_scanner), ),
const SizedBox(height: 8),
Text(
t.companionRemote.pairing.discoveryDescription,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
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),
),
),
],
), ),
ButtonSegment( ),
value: _manualTabIndex,
label: Text(t.companionRemote.pairing.manual),
icon: const Icon(Icons.keyboard),
),
],
selected: {_selectedTab},
onSelectionChanged: (Set<int> selection) {
setState(() {
_selectedTab = selection.first;
});
},
), ),
Expanded(child: _buildTabContent()), const SizedBox(height: 16),
],
// Discovered hosts
_buildDiscoverySection(),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
// Manual entry fallback
_buildManualEntrySection(),
], ],
), ),
); );
} }
Widget _buildTabContent() { Widget _buildDiscoverySection() {
if (_selectedTab == _scanTabIndex && _canScan) return _buildScanTab(); if (!_cryptoReady) {
return _buildManualEntryTab(); return Card(
} child: Padding(
padding: const EdgeInsets.all(24.0),
Widget _buildScanTab() {
return Column(
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: MobileScanner(
controller: _scannerController ??= MobileScannerController(),
onDetect: (capture) {
final barcode = capture.barcodes.firstOrNull;
if (barcode?.rawValue != null) {
_handleQrCode(barcode!.rawValue!);
}
},
errorBuilder: (context, error) {
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
? t.companionRemote.pairing.cameraPermissionRequired
: t.companionRemote.pairing.cameraError(
error: error.errorDetails?.message ?? error.errorCode.name,
),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
},
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
child: Column( child: Column(
children: [ children: [
const Icon(Icons.warning_amber, size: 48, color: Colors.orange),
const SizedBox(height: 12),
Text( Text(
t.companionRemote.pairing.scanInstruction, t.companionRemote.pairing.cryptoInitFailed,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center, 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),
),
),
],
),
),
),
],
], ],
), ),
), ),
);
}
if (_isSearching && _hosts.isEmpty) {
return Card(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(strokeWidth: 3),
),
const SizedBox(height: 16),
Text(
t.companionRemote.pairing.searchingForDevices,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}
if (_hosts.isEmpty) {
return Card(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Icon(Icons.devices_other, size: 48, color: Colors.grey),
const SizedBox(height: 12),
Text(
t.companionRemote.pairing.noDevicesFound,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
t.companionRemote.pairing.noDevicesHint,
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.companionRemote.pairing.availableDevices,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
..._hosts.map((host) => Card(
child: ListTile(
leading: Icon(_platformIcon(host.platform), size: 32),
title: Text(host.name),
subtitle: Text(host.platform),
trailing: _isConnecting
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.arrow_forward),
onTap: _isConnecting ? null : () => _connectToHost(host),
),
)),
], ],
); );
} }
Widget _buildManualEntryTab() { Widget _buildManualEntrySection() {
return SingleChildScrollView( return Column(
padding: const EdgeInsets.all(24.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Form( children: [
key: _formKey, InkWell(
child: Column( onTap: () => setState(() => _showManualEntry = !_showManualEntry),
crossAxisAlignment: CrossAxisAlignment.stretch, child: Row(
children: [ children: [
Icon(Icons.keyboard, size: 64, color: Theme.of(context).colorScheme.primary), Icon(
const SizedBox(height: 24), _showManualEntry ? Icons.expand_less : Icons.expand_more,
Text( color: Theme.of(context).colorScheme.primary,
t.companionRemote.pairing.pairWithDesktop, ),
style: Theme.of(context).textTheme.headlineMedium, const SizedBox(width: 8),
textAlign: TextAlign.center, Text(
), t.companionRemote.pairing.manualConnection,
const SizedBox(height: 8), style: Theme.of(context).textTheme.titleMedium?.copyWith(
Text( color: Theme.of(context).colorScheme.primary,
t.companionRemote.pairing.enterSessionDetails, ),
style: Theme.of(context).textTheme.bodyMedium, ),
textAlign: TextAlign.center, ],
), ),
const SizedBox(height: 32), ),
if (_errorMessage != null) ...[ if (_showManualEntry) ...[
Card( const SizedBox(height: 16),
color: Theme.of(context).colorScheme.errorContainer, Form(
child: Padding( key: _formKey,
padding: const EdgeInsets.all(16.0), child: Column(
child: Row( crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.onErrorContainer), TextFormField(
const SizedBox(width: 12), controller: _hostAddressController,
Expanded( decoration: InputDecoration(
child: Text( labelText: t.companionRemote.session.hostAddress,
_errorMessage!, hintText: t.companionRemote.pairing.hostAddressHint,
style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer), border: const OutlineInputBorder(),
), prefixIcon: const Icon(Icons.computer),
),
],
), ),
validator: (value) {
if (value == null || value.isEmpty) {
return t.companionRemote.pairing.validationHostRequired;
}
final parts = value.split(':');
if (parts.length != 2) {
return t.companionRemote.pairing.validationHostFormat;
}
return null;
},
enabled: !_isConnecting,
), ),
), const SizedBox(height: 16),
const SizedBox(height: 16), FilledButton.icon(
], onPressed: _isConnecting ? null : _connectManual,
TextFormField( icon: _isConnecting
controller: _hostAddressController, ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
decoration: InputDecoration( : const Icon(Icons.link),
labelText: t.companionRemote.session.hostAddress, label: Text(_isConnecting ? t.companionRemote.pairing.connecting : t.common.connect),
hintText: t.companionRemote.pairing.hostAddressHint,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.computer),
suffixIcon: IconButton(
icon: const Icon(Icons.paste),
onPressed: () => _pasteFromClipboard(_hostAddressController),
tooltip: t.common.paste,
), ),
),
validator: (value) {
if (value == null || value.isEmpty) {
return t.companionRemote.pairing.validationHostRequired;
}
// Validate IP:port format
final parts = value.split(':');
if (parts.length != 2) {
return t.companionRemote.pairing.validationHostFormat;
}
return null;
},
enabled: !_isConnecting,
),
const SizedBox(height: 16),
TextFormField(
controller: _sessionIdController,
decoration: InputDecoration(
labelText: t.companionRemote.session.sessionId,
hintText: t.companionRemote.pairing.sessionIdHint,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.vpn_key),
suffixIcon: IconButton(
icon: const Icon(Icons.paste),
onPressed: () => _pasteFromClipboard(_sessionIdController),
tooltip: t.common.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 t.companionRemote.pairing.validationSessionIdRequired;
}
if (value.length != 8) {
return t.companionRemote.pairing.validationSessionIdLength;
}
return null;
},
enabled: !_isConnecting,
), ),
const SizedBox(height: 16), ),
TextFormField( ],
controller: _pinController, ],
decoration: InputDecoration(
labelText: t.companionRemote.session.pin,
hintText: t.companionRemote.pairing.pinHint,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: const Icon(Icons.paste),
onPressed: () => _pasteFromClipboard(_pinController),
tooltip: t.common.paste,
),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)],
validator: (value) {
if (value == null || value.isEmpty) {
return t.companionRemote.pairing.validationPinRequired;
}
if (value.length != 6) {
return t.companionRemote.pairing.validationPinLength;
}
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 ? t.companionRemote.pairing.connecting : t.common.connect),
),
const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 16),
Text(t.companionRemote.pairing.tips, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
_buildTipCard(context, Icons.computer, t.companionRemote.pairing.tipDesktop),
if (_canScan) ...[
const SizedBox(height: 8),
_buildTipCard(context, Icons.qr_code, t.companionRemote.pairing.tipScan),
],
const SizedBox(height: 8),
_buildTipCard(context, Icons.wifi, t.companionRemote.pairing.tipWifi),
],
),
),
);
}
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)),
],
),
),
); );
} }
} }
+18
View File
@@ -436,6 +436,9 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
} }
}; };
// Auto-start host server if setting enabled
_autoStartCompanionRemoteServer(companionRemote);
final receiver = CompanionRemoteReceiver.instance; final receiver = CompanionRemoteReceiver.instance;
receiver.onTabNext = () { receiver.onTabNext = () {
@@ -466,6 +469,21 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
}; };
} }
Future<void> _autoStartCompanionRemoteServer(CompanionRemoteProvider companionRemote) async {
try {
final settings = await SettingsService.getInstance();
if (!settings.getEnableCompanionRemoteServer()) return;
if (!mounted) return;
final home = context.read<UserProfileProvider>().home;
if (await companionRemote.ensureCryptoReady(home)) {
await companionRemote.startHostServer();
}
} catch (e) {
appLogger.e('CompanionRemote: Failed to auto-start server', error: e);
}
}
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
@@ -45,6 +45,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
String _creditsPattern = settings.SettingsService.defaultCreditsPattern; String _creditsPattern = settings.SettingsService.defaultCreditsPattern;
int _maxVolume = 100; int _maxVolume = 100;
bool _enableDiscordRPC = false; bool _enableDiscordRPC = false;
bool _enableCompanionRemoteServer = false;
bool _autoPip = true; bool _autoPip = true;
bool _matchContentFrameRate = false; bool _matchContentFrameRate = false;
bool _matchRefreshRate = false; bool _matchRefreshRate = false;
@@ -84,6 +85,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
_creditsPattern = _settingsService.getCreditsPattern(); _creditsPattern = _settingsService.getCreditsPattern();
_maxVolume = _settingsService.getMaxVolume(); _maxVolume = _settingsService.getMaxVolume();
_enableDiscordRPC = _settingsService.getEnableDiscordRPC(); _enableDiscordRPC = _settingsService.getEnableDiscordRPC();
_enableCompanionRemoteServer = _settingsService.getEnableCompanionRemoteServer();
_autoPip = _settingsService.getAutoPip(); _autoPip = _settingsService.getAutoPip();
_matchContentFrameRate = _settingsService.getMatchContentFrameRate(); _matchContentFrameRate = _settingsService.getMatchContentFrameRate();
_matchRefreshRate = _settingsService.getMatchRefreshRate(); _matchRefreshRate = _settingsService.getMatchRefreshRate();
@@ -142,6 +144,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
// --- Behavior --- // --- Behavior ---
SettingsSectionHeader(t.settings.behavior), SettingsSectionHeader(t.settings.behavior),
if (DiscordRPCService.isAvailable) _buildDiscordRPC(), if (DiscordRPCService.isAvailable) _buildDiscordRPC(),
if (PlatformDetector.shouldActAsRemoteHost(context)) _buildCompanionRemoteServer(),
_buildRememberTrackSelections(), _buildRememberTrackSelections(),
if (!isMobile) _buildClickVideoTogglesPlayback(), if (!isMobile) _buildClickVideoTogglesPlayback(),
@@ -494,6 +497,19 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
); );
} }
Widget _buildCompanionRemoteServer() {
return SwitchListTile(
secondary: const AppIcon(Symbols.phone_android_rounded, fill: 1),
title: Text(t.settings.companionRemoteServer),
subtitle: Text(t.settings.companionRemoteServerDescription),
value: _enableCompanionRemoteServer,
onChanged: (value) async {
setState(() => _enableCompanionRemoteServer = value);
await _settingsService.setEnableCompanionRemoteServer(value);
},
);
}
Widget _buildRememberTrackSelections() { Widget _buildRememberTrackSelections() {
return SwitchListTile( return SwitchListTile(
secondary: const AppIcon(Symbols.bookmark_rounded, fill: 1), secondary: const AppIcon(Symbols.bookmark_rounded, fill: 1),
@@ -10,6 +10,7 @@ import '../../models/companion_remote/remote_command.dart';
import '../../models/companion_remote/remote_session.dart'; import '../../models/companion_remote/remote_session.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../base_peer_service.dart'; import '../base_peer_service.dart';
import 'remote_auth_service.dart';
// Re-export so callers that import from here get the types. // Re-export so callers that import from here get the types.
export '../base_peer_service.dart' show PeerError, PeerErrorType; export '../base_peer_service.dart' show PeerError, PeerErrorType;
@@ -27,12 +28,16 @@ class CompanionRemotePeerService with KeepaliveMixin {
// Client-side (remote) fields // Client-side (remote) fields
IOWebSocketChannel? _channel; IOWebSocketChannel? _channel;
String? _sessionId;
String? _pin;
String? _myPeerId; String? _myPeerId;
String? _hostAddress; // Format: "ip:port" String? _hostAddress; // Format: "ip:port"
RemoteSessionRole? _role; RemoteSessionRole? _role;
// Encrypted channel state
List<int>? _sessionEncKey;
int _sendCounter = 0;
int _recvCounter = 0;
bool _isAuthenticated = false;
final _commandReceivedController = StreamController<RemoteCommand>.broadcast(); final _commandReceivedController = StreamController<RemoteCommand>.broadcast();
final _deviceConnectedController = StreamController<RemoteDevice>.broadcast(); final _deviceConnectedController = StreamController<RemoteDevice>.broadcast();
final _deviceDisconnectedController = StreamController<void>.broadcast(); final _deviceDisconnectedController = StreamController<void>.broadcast();
@@ -45,9 +50,9 @@ class CompanionRemotePeerService with KeepaliveMixin {
@override @override
Duration get pongTimeout => Duration.zero; // No pong timeout; host just replies inline Duration get pongTimeout => Duration.zero; // No pong timeout; host just replies inline
// Auth rate limiting // Auth rate limiting (per source IP)
int _failedAuthAttempts = 0; final Map<String, int> _failedAuthAttempts = {};
DateTime? _authLockoutUntil; final Map<String, DateTime> _authLockouts = {};
static const int _maxFailedAuthAttempts = 5; static const int _maxFailedAuthAttempts = 5;
static const Duration _authLockoutDuration = Duration(seconds: 30); static const Duration _authLockoutDuration = Duration(seconds: 30);
@@ -57,25 +62,12 @@ class CompanionRemotePeerService with KeepaliveMixin {
Stream<RemotePeerError> get onError => _errorController.stream; Stream<RemotePeerError> get onError => _errorController.stream;
Stream<RemoteSessionStatus> get onConnectionStateChanged => _connectionStateController.stream; Stream<RemoteSessionStatus> get onConnectionStateChanged => _connectionStateController.stream;
String? get sessionId => _sessionId;
String? get pin => _pin;
String? get myPeerId => _myPeerId; String? get myPeerId => _myPeerId;
String? get hostAddress => _hostAddress; String? get hostAddress => _hostAddress;
RemoteSessionRole? get role => _role; RemoteSessionRole? get role => _role;
bool get isHost => _role == RemoteSessionRole.host; bool get isHost => _role == RemoteSessionRole.host;
bool get isConnected => _clientSocket != null || (_channel != null && _channel?.closeCode == null); bool get isConnected => _clientSocket != null || (_channel != null && _channel?.closeCode == 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();
}
Future<List<String>> _getAllLocalIpAddresses() async { Future<List<String>> _getAllLocalIpAddresses() async {
try { try {
final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4); final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4);
@@ -109,21 +101,22 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
} }
Future<({String sessionId, String pin, List<String> addresses})> createSession( /// Create a host session — starts WebSocket server, returns local addresses and port.
Future<({List<String> addresses, int port})> createSession(
String deviceName, String deviceName,
String platform, String platform,
List<int> homeSecret,
String clientIdentifier,
List<String> homeUserUUIDs,
) async { ) async {
if (_server != null) { if (_server != null) {
await disconnect(); await disconnect();
} }
_role = RemoteSessionRole.host; _role = RemoteSessionRole.host;
_sessionId = _generateSessionId(); _myPeerId = 'host';
_pin = _generatePin();
_myPeerId = 'host-$_sessionId';
try { try {
// Try preferred port first, fallback to OS-assigned port
const int preferredPort = 48632; const int preferredPort = 48632;
try { try {
@@ -141,12 +134,20 @@ class CompanionRemotePeerService with KeepaliveMixin {
appLogger.d('CompanionRemote: Host server started, addresses: $addresses'); appLogger.d('CompanionRemote: Host server started, addresses: $addresses');
// Listen for WebSocket connections
_server!.listen((HttpRequest request) async { _server!.listen((HttpRequest request) async {
if (request.uri.path == '/ws') { if (request.uri.path == '/ws') {
try { try {
final socket = await WebSocketTransformer.upgrade(request); final socket = await WebSocketTransformer.upgrade(request);
_handleNewWebSocketConnection(socket, deviceName, platform); final sourceIp = request.connectionInfo?.remoteAddress.address ?? 'unknown';
_handleNewWebSocketConnection(
socket,
deviceName,
platform,
homeSecret,
clientIdentifier,
homeUserUUIDs,
sourceIp,
);
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to upgrade WebSocket', error: e); appLogger.e('CompanionRemote: Failed to upgrade WebSocket', error: e);
} }
@@ -158,7 +159,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
_connectionStateController.add(RemoteSessionStatus.connected); _connectionStateController.add(RemoteSessionStatus.connected);
return (sessionId: _sessionId!, pin: _pin!, addresses: addresses); return (addresses: addresses, port: port);
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to create server', error: e); appLogger.e('CompanionRemote: Failed to create server', error: e);
_errorController.add( _errorController.add(
@@ -172,11 +173,36 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
} }
void _handleNewWebSocketConnection(WebSocket socket, String hostDeviceName, String hostPlatform) { void _handleNewWebSocketConnection(
appLogger.d('CompanionRemote: New WebSocket connection'); WebSocket socket,
String hostDeviceName,
String hostPlatform,
List<int> homeSecret,
String hostClientId,
List<String> homeUserUUIDs,
String sourceIp,
) {
appLogger.d('CompanionRemote: New WebSocket connection from $sourceIp');
bool isAuthenticated = false; bool isAuthenticated = false;
Timer? authTimeout; Timer? authTimeout;
final auth = RemoteAuthService.instance;
final hostNonce = auth.generateNonce();
// Check rate limiting
final lockout = _authLockouts[sourceIp];
if (lockout != null && DateTime.now().isBefore(lockout)) {
appLogger.w('CompanionRemote: Connection from $sourceIp rejected (rate limited)');
socket.close(4005, 'Rate limited');
return;
}
// Send challenge: hostNonce + hostClientId
socket.add(jsonEncode({
'type': 'challenge',
'nonce': base64Encode(hostNonce),
'hostClientId': hostClientId,
}));
// Authentication timeout // Authentication timeout
authTimeout = Timer(const Duration(seconds: 10), () { authTimeout = Timer(const Duration(seconds: 10), () {
@@ -187,77 +213,107 @@ class CompanionRemotePeerService with KeepaliveMixin {
}); });
socket.listen( socket.listen(
(data) { (data) async {
try { try {
final json = jsonDecode(data as String) as Map<String, dynamic>;
if (!isAuthenticated) { if (!isAuthenticated) {
// First message must be authentication final json = jsonDecode(data as String) as Map<String, dynamic>;
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?; if (json['type'] == 'auth') {
final pin = json['pin'] as String?; final authTag = json['authTag'] as String?;
final clientNonceB64 = json['clientNonce'] as String?;
final userUUID = json['userUUID'] as String?;
final clientIdentifier = json['clientIdentifier'] as String?;
final deviceName = json['deviceName'] as String?; final deviceName = json['deviceName'] as String?;
final platform = json['platform'] as String?; final platform = json['platform'] as String?;
if (sessionId == _sessionId && pin == _pin) { if (authTag == null ||
_failedAuthAttempts = 0; clientNonceB64 == null ||
isAuthenticated = true; userUUID == null ||
authTimeout?.cancel(); clientIdentifier == null ||
deviceName == null ||
// Close existing client if present platform == null) {
if (_clientSocket != null) { socket.add(jsonEncode({'type': 'authFailed'}));
appLogger.d('CompanionRemote: Replacing existing client connection'); socket.close(4003, 'Authentication failed');
_clientSocket!.close(4004, 'Replaced by new connection'); return;
}
_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 {
_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');
} }
final clientNonce = base64Decode(clientNonceB64);
// Verify userUUID is in home users list
if (!homeUserUUIDs.contains(userUUID)) {
_recordFailedAuth(sourceIp);
appLogger.w('CompanionRemote: Auth failed — unknown user');
socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed');
return;
}
// Verify auth tag
final valid = auth.verifyAuthTag(
authTag: authTag,
homeSecret: homeSecret,
hostNonce: hostNonce,
clientNonce: clientNonce,
hostClientId: hostClientId,
userUUID: userUUID,
clientIdentifier: clientIdentifier,
deviceName: deviceName,
platform: platform,
);
if (!valid) {
_recordFailedAuth(sourceIp);
appLogger.w('CompanionRemote: Auth failed — invalid auth tag');
socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed');
return;
}
// Auth success — derive per-session encryption key
_failedAuthAttempts.remove(sourceIp);
isAuthenticated = true;
authTimeout?.cancel();
final sessionEncKey = await auth.deriveSessionEncKey(homeSecret, hostNonce, clientNonce);
// Close existing client if present
if (_clientSocket != null) {
appLogger.d('CompanionRemote: Replacing existing client connection');
_clientSocket!.close(4004, 'Replaced by new connection');
}
_clientSocket = socket;
_sessionEncKey = sessionEncKey;
_sendCounter = 0;
_recvCounter = 0;
_isAuthenticated = true;
appLogger.d('CompanionRemote: Client authenticated: $deviceName ($platform)');
// Send encrypted authSuccess
await _sendEncryptedToSocket(socket, jsonEncode({'type': 'authSuccess'}));
// Notify connection
final device = RemoteDevice(
id: 'remote-client',
name: deviceName,
platform: platform,
);
_deviceConnectedController.add(device);
_connectionStateController.add(RemoteSessionStatus.connected);
// Send device info
sendDeviceInfo(hostDeviceName, hostPlatform);
} else { } else {
appLogger.w('CompanionRemote: Expected auth, got ${json['type']}'); appLogger.w('CompanionRemote: Expected auth, got ${json['type']}');
socket.close(4002, 'Authentication required'); socket.close(4002, 'Authentication required');
} }
} else { } else {
// Handle regular commands // Encrypted command — data is binary
final decrypted = await _decryptIncoming(data);
if (decrypted == null) return;
final json = jsonDecode(decrypted) as Map<String, dynamic>;
final command = RemoteCommand.fromJson(json); final command = RemoteCommand.fromJson(json);
appLogger.d('CompanionRemote: Received command: ${command.type}'); appLogger.d('CompanionRemote: Received command: ${command.type}');
@@ -280,6 +336,8 @@ class CompanionRemotePeerService with KeepaliveMixin {
appLogger.d('CompanionRemote: WebSocket connection closed'); appLogger.d('CompanionRemote: WebSocket connection closed');
if (isAuthenticated) { if (isAuthenticated) {
_clientSocket = null; _clientSocket = null;
_sessionEncKey = null;
_isAuthenticated = false;
_deviceDisconnectedController.add(null); _deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected); _connectionStateController.add(RemoteSessionStatus.disconnected);
stopKeepalive(); stopKeepalive();
@@ -299,18 +357,35 @@ class CompanionRemotePeerService with KeepaliveMixin {
); );
} }
Future<void> joinSession(String sessionId, String pin, String deviceName, String platform, String hostAddress) async { void _recordFailedAuth(String sourceIp) {
final attempts = (_failedAuthAttempts[sourceIp] ?? 0) + 1;
_failedAuthAttempts[sourceIp] = attempts;
if (attempts >= _maxFailedAuthAttempts) {
_authLockouts[sourceIp] = DateTime.now().add(_authLockoutDuration);
appLogger.w('CompanionRemote: IP $sourceIp locked out for ${_authLockoutDuration.inSeconds}s');
}
}
/// Join a host session as a remote client.
Future<void> joinSession(
String deviceName,
String platform,
String hostAddress,
List<int> homeSecret,
String hostClientId,
String userUUID,
String clientIdentifier,
) async {
if (_channel != null) { if (_channel != null) {
await disconnect(); await disconnect();
} }
_role = RemoteSessionRole.remote; _role = RemoteSessionRole.remote;
_sessionId = sessionId.toUpperCase();
_pin = pin;
_hostAddress = hostAddress; _hostAddress = hostAddress;
_myPeerId = 'remote-${Random.secure().nextInt(99999)}'; _myPeerId = 'remote-${Random.secure().nextInt(99999)}';
final completer = Completer<void>(); final completer = Completer<void>();
final auth = RemoteAuthService.instance;
try { try {
final url = 'ws://$hostAddress/ws'; final url = 'ws://$hostAddress/ws';
@@ -321,51 +396,19 @@ class CompanionRemotePeerService with KeepaliveMixin {
_channel = IOWebSocketChannel.connect(Uri.parse(url)); _channel = IOWebSocketChannel.connect(Uri.parse(url));
await _channel!.ready; await _channel!.ready;
// Send authentication message List<int>? hostNonce;
final authMessage = jsonEncode({ List<int>? clientNonce;
'type': 'auth', String? receivedHostClientId;
'sessionId': _sessionId,
'pin': _pin,
'deviceName': deviceName,
'platform': platform,
});
_channel!.sink.add(authMessage);
// Listen for messages
_channel!.stream.listen( _channel!.stream.listen(
(data) { (data) async {
try { try {
final json = jsonDecode(data as String) as Map<String, dynamic>; if (_isAuthenticated) {
final messageType = json['type'] as String?; // Post-auth: all messages are encrypted binary
final decrypted = await _decryptIncoming(data);
if (decrypted == null) return;
if (messageType == 'authSuccess') { final json = jsonDecode(decrypted) as Map<String, dynamic>;
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
startKeepalive();
} 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); final command = RemoteCommand.fromJson(json);
appLogger.d('CompanionRemote: Received command: ${command.type}'); appLogger.d('CompanionRemote: Received command: ${command.type}');
@@ -378,6 +421,89 @@ class CompanionRemotePeerService with KeepaliveMixin {
if (command.type == RemoteCommandType.ping) { if (command.type == RemoteCommandType.ping) {
_sendPong(); _sendPong();
} }
} else if (_sessionEncKey != null) {
// Keys derived, waiting for encrypted authSuccess
final decrypted = await _decryptIncoming(data);
if (decrypted == null) return;
final json = jsonDecode(decrypted) as Map<String, dynamic>;
if (json['type'] == 'authSuccess') {
_isAuthenticated = true;
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);
sendDeviceInfo(deviceName, platform);
startKeepalive();
} else if (json['type'] == 'authFailed') {
if (!completer.isCompleted) {
completer.completeError(
const RemotePeerError(type: RemotePeerErrorType.authFailed, message: 'Authentication failed'),
);
}
}
} else {
// Pre-auth: plaintext handshake
final json = jsonDecode(data as String) as Map<String, dynamic>;
final messageType = json['type'] as String?;
if (messageType == 'challenge') {
hostNonce = base64Decode(json['nonce'] as String);
receivedHostClientId = json['hostClientId'] as String;
clientNonce = auth.generateNonce();
if (hostClientId.isNotEmpty && receivedHostClientId != hostClientId) {
appLogger.w('CompanionRemote: Host client ID mismatch');
if (!completer.isCompleted) {
completer.completeError(
const RemotePeerError(type: RemotePeerErrorType.authFailed, message: 'Host identity mismatch'),
);
}
return;
}
final authTag = auth.computeAuthTag(
homeSecret: homeSecret,
hostNonce: hostNonce!,
clientNonce: clientNonce!,
hostClientId: receivedHostClientId!,
userUUID: userUUID,
clientIdentifier: clientIdentifier,
deviceName: deviceName,
platform: platform,
);
_channel!.sink.add(jsonEncode({
'type': 'auth',
'clientNonce': base64Encode(clientNonce!),
'userUUID': userUUID,
'clientIdentifier': clientIdentifier,
'deviceName': deviceName,
'platform': platform,
'authTag': authTag,
}));
_sessionEncKey = await auth.deriveSessionEncKey(homeSecret, hostNonce!, clientNonce!);
_sendCounter = 0;
_recvCounter = 0;
} else if (messageType == 'authFailed') {
appLogger.w('CompanionRemote: Authentication failed');
if (!completer.isCompleted) {
completer.completeError(
const RemotePeerError(type: RemotePeerErrorType.authFailed, message: 'Authentication failed'),
);
}
_errorController.add(
const RemotePeerError(type: RemotePeerErrorType.authFailed, message: 'Authentication failed'),
);
_connectionStateController.add(RemoteSessionStatus.error);
}
} }
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to parse message', error: e); appLogger.e('CompanionRemote: Failed to parse message', error: e);
@@ -387,8 +513,9 @@ class CompanionRemotePeerService with KeepaliveMixin {
appLogger.d('CompanionRemote: Connection closed'); appLogger.d('CompanionRemote: Connection closed');
_deviceDisconnectedController.add(null); _deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected); _connectionStateController.add(RemoteSessionStatus.disconnected);
_isAuthenticated = false;
_sessionEncKey = null;
stopKeepalive(); stopKeepalive();
// Reconnection is handled by CompanionRemoteProvider
}, },
onError: (error) { onError: (error) {
appLogger.e('CompanionRemote: Connection error', error: error); appLogger.e('CompanionRemote: Connection error', error: error);
@@ -422,7 +549,6 @@ class CompanionRemotePeerService with KeepaliveMixin {
return completer.future.timeout( return completer.future.timeout(
const Duration(seconds: 15), const Duration(seconds: 15),
onTimeout: () async { onTimeout: () async {
// Clean up channel on timeout
if (_channel != null) { if (_channel != null) {
try { try {
await _channel!.sink.close(); await _channel!.sink.close();
@@ -435,21 +561,23 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
/// Race WebSocket connections to multiple host addresses in parallel. /// Race WebSocket connections to multiple host addresses in parallel.
/// Returns the winning address and sets up a proper managed connection via [joinSession].
Future<String> joinSessionRacing( Future<String> joinSessionRacing(
String sessionId,
String pin,
String deviceName, String deviceName,
String platform, String platform,
List<String> hostAddresses, List<String> hostAddresses,
List<int> homeSecret,
String hostClientId,
String userUUID,
String clientIdentifier,
) async { ) async {
if (hostAddresses.length == 1) { if (hostAddresses.length == 1) {
await joinSession(sessionId, pin, deviceName, platform, hostAddresses.first); await joinSession(deviceName, platform, hostAddresses.first, homeSecret, hostClientId, userUUID, clientIdentifier);
return hostAddresses.first; return hostAddresses.first;
} }
appLogger.d('CompanionRemote: Racing connections to ${hostAddresses.length} addresses'); appLogger.d('CompanionRemote: Racing connections to ${hostAddresses.length} addresses');
// Race: try to connect to all addresses, first one to get a challenge wins
final completer = Completer<String>(); final completer = Completer<String>();
final channels = <IOWebSocketChannel>[]; final channels = <IOWebSocketChannel>[];
final subs = <StreamSubscription>[]; final subs = <StreamSubscription>[];
@@ -471,20 +599,12 @@ class CompanionRemotePeerService with KeepaliveMixin {
final channel = IOWebSocketChannel.connect(Uri.parse(url), connectTimeout: const Duration(seconds: 5)); final channel = IOWebSocketChannel.connect(Uri.parse(url), connectTimeout: const Duration(seconds: 5));
channels.add(channel); channels.add(channel);
// Send auth immediately
channel.sink.add(jsonEncode({
'type': 'auth',
'sessionId': sessionId.toUpperCase(),
'pin': pin,
'deviceName': deviceName,
'platform': platform,
}));
final sub = channel.stream.listen( final sub = channel.stream.listen(
(data) { (data) {
try { try {
final json = jsonDecode(data as String) as Map<String, dynamic>; final json = jsonDecode(data as String) as Map<String, dynamic>;
if (json['type'] == 'authSuccess' && !completer.isCompleted) { // First address to send us a challenge wins the race
if (json['type'] == 'challenge' && !completer.isCompleted) {
appLogger.d('CompanionRemote: Race winner: $address'); appLogger.d('CompanionRemote: Race winner: $address');
completer.complete(address); completer.complete(address);
} }
@@ -507,11 +627,12 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
try { try {
final winner = await completer.future.namedTimeout(const Duration(seconds: 10), operation: 'CompanionRemote race connect'); final winner =
await completer.future.namedTimeout(const Duration(seconds: 10), operation: 'CompanionRemote race connect');
cleanup(); cleanup();
// Now set up the proper managed connection on the winning address // Set up the proper managed connection on the winning address
await joinSession(sessionId, pin, deviceName, platform, winner); await joinSession(deviceName, platform, winner, homeSecret, hostClientId, userUUID, clientIdentifier);
return winner; return winner;
} on TimeoutException { } on TimeoutException {
cleanup(); cleanup();
@@ -522,6 +643,49 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
} }
// ── Encrypted send/receive ──
// Serializes async sends to prevent counter interleaving
Future<void>? _sendChain;
Future<List<int>> _encryptOutgoing(String plaintext) async {
final encrypted = await RemoteAuthService.instance.encrypt(
_sessionEncKey!,
utf8.encode(plaintext),
isHost: _role == RemoteSessionRole.host,
counter: _sendCounter,
);
_sendCounter++;
return encrypted;
}
Future<void> _sendEncryptedToSocket(WebSocket socket, String plaintext) async {
if (_sessionEncKey == null) return;
final encrypted = await _encryptOutgoing(plaintext);
socket.add(encrypted);
}
Future<String?> _decryptIncoming(dynamic data) async {
if (_sessionEncKey == null) return null;
try {
final auth = RemoteAuthService.instance;
final bytes = data is List<int> ? data : utf8.encode(data as String);
final decrypted = await auth.decrypt(
bytes,
_sessionEncKey!,
fromHost: _role == RemoteSessionRole.remote, // If we're remote, incoming is from host
expectedCounter: _recvCounter,
);
_recvCounter++;
return utf8.decode(decrypted);
} catch (e) {
appLogger.e('CompanionRemote: Decryption failed (counter=$_recvCounter)', error: e);
return null;
}
}
// ── Commands ──
@override @override
void sendPing() { void sendPing() {
if (isConnected) { if (isConnected) {
@@ -559,28 +723,34 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
void sendCommand(RemoteCommand command) { void sendCommand(RemoteCommand command) {
try { if (_sessionEncKey == null || !_isAuthenticated) {
final json = jsonEncode(command.toJson()); appLogger.w('CompanionRemote: No connection to send command');
return;
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(
RemotePeerError(
type: RemotePeerErrorType.dataChannelError,
message: 'Failed to send command: $e',
originalError: e,
),
);
} }
// Chain sends to prevent counter interleaving from concurrent async encrypts
_sendChain = (_sendChain ?? Future.value()).then((_) async {
try {
final json = jsonEncode(command.toJson());
final encrypted = await _encryptOutgoing(json);
if (_role == RemoteSessionRole.host && _clientSocket != null) {
_clientSocket!.add(encrypted);
} else if (_role == RemoteSessionRole.remote && _channel != null) {
_channel!.sink.add(encrypted);
}
appLogger.d('CompanionRemote: Sent command: ${command.type}');
} catch (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<void> disconnect() async { Future<void> disconnect() async {
@@ -607,15 +777,23 @@ class CompanionRemotePeerService with KeepaliveMixin {
_server = null; _server = null;
} }
_sessionId = null;
_pin = null;
_myPeerId = null; _myPeerId = null;
_hostAddress = null; _hostAddress = null;
_role = null; _role = null;
_sessionEncKey = null;
_sendCounter = 0;
_recvCounter = 0;
_isAuthenticated = false;
_sendChain = null;
_failedAuthAttempts.clear();
_authLockouts.clear();
_connectionStateController.add(RemoteSessionStatus.disconnected); _connectionStateController.add(RemoteSessionStatus.disconnected);
} }
/// Whether the HTTP server is currently running.
bool get isServerRunning => _server != null;
Future<void> dispose() async { Future<void> dispose() async {
await disconnect(); await disconnect();
await _commandReceivedController.close(); await _commandReceivedController.close();
@@ -0,0 +1,284 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import '../../utils/app_logger.dart';
import 'remote_auth_service.dart';
/// A host discovered on the LAN via UDP broadcast.
class DiscoveredHost {
final String clientId;
final String name;
final String platform;
final int port;
final List<String> ips;
DateTime lastSeen;
DiscoveredHost({
required this.clientId,
required this.name,
required this.platform,
required this.port,
required this.ips,
DateTime? lastSeen,
}) : lastSeen = lastSeen ?? DateTime.now();
List<String> get addresses => ips.map((ip) => '$ip:$port').toList();
}
/// UDP-based LAN discovery for companion remote.
///
/// Hosts broadcast authenticated beacons; clients listen and filter
/// by matching Plex home membership.
class LanDiscoveryService {
static const int discoveryPort = 48633;
static const int _broadcastIntervalSeconds = 3;
static const int _staleTimeoutSeconds = 10;
static const int _beaconVersion = 1;
// Broadcaster state (host)
RawDatagramSocket? _broadcastSocket;
Timer? _broadcastTimer;
// Listener state (client)
RawDatagramSocket? _listenSocket;
Timer? _staleCleanupTimer;
final Map<String, DiscoveredHost> _discoveredHosts = {};
final _hostsController = StreamController<List<DiscoveredHost>>.broadcast();
/// Whether the broadcaster is currently active.
bool get isBroadcasting => _broadcastTimer != null;
/// Whether the listener is currently active.
bool get isListening => _listenSocket != null;
// ── Host: Broadcasting ──
Future<void> startBroadcasting({
required List<int> discoveryKey,
required String deviceName,
required String platform,
required String clientId,
required int wsPort,
required List<String> ips,
}) async {
await stopBroadcasting();
try {
_broadcastSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
_broadcastSocket!.broadcastEnabled = true;
appLogger.d('LanDiscovery: Broadcasting started on port $discoveryPort');
// Send immediately, then periodically
_sendBeacon(discoveryKey, deviceName, platform, clientId, wsPort, ips);
_broadcastTimer = Timer.periodic(
const Duration(seconds: _broadcastIntervalSeconds),
(_) => _sendBeacon(discoveryKey, deviceName, platform, clientId, wsPort, ips),
);
} catch (e) {
appLogger.e('LanDiscovery: Failed to start broadcasting', error: e);
await stopBroadcasting();
}
}
void _sendBeacon(
List<int> discoveryKey,
String deviceName,
String platform,
String clientId,
int wsPort,
List<String> ips,
) {
if (_broadcastSocket == null) return;
try {
final auth = RemoteAuthService.instance;
final homeHash = auth.computeDiscoveryTag(discoveryKey);
final beaconHmac = auth.computeBeaconHmac(
discoveryKey: discoveryKey,
version: _beaconVersion,
homeHash: homeHash,
name: deviceName,
platform: platform,
clientId: clientId,
port: wsPort,
ips: ips,
);
final packet = jsonEncode({
'app': 'plezy',
'v': _beaconVersion,
'homeHash': homeHash,
'name': deviceName,
'platform': platform,
'clientId': clientId,
'port': wsPort,
'ips': ips,
'hmac': beaconHmac,
});
final data = utf8.encode(packet);
_broadcastSocket!.send(data, InternetAddress('255.255.255.255'), discoveryPort);
} catch (e) {
appLogger.e('LanDiscovery: Failed to send beacon', error: e);
}
}
Future<void> stopBroadcasting() async {
_broadcastTimer?.cancel();
_broadcastTimer = null;
_broadcastSocket?.close();
_broadcastSocket = null;
appLogger.d('LanDiscovery: Broadcasting stopped');
}
// ── Client: Listening ──
/// Start listening for host beacons.
/// Returns a stream of currently-visible hosts, updated on each beacon or stale cleanup.
Stream<List<DiscoveredHost>> startListening({
required List<int> discoveryKey,
}) {
_stopListeningInternal();
_discoveredHosts.clear();
_bindListener(discoveryKey);
// Periodically remove stale hosts
_staleCleanupTimer = Timer.periodic(const Duration(seconds: 2), (_) {
final now = DateTime.now();
final staleIds = <String>[];
for (final entry in _discoveredHosts.entries) {
if (now.difference(entry.value.lastSeen).inSeconds > _staleTimeoutSeconds) {
staleIds.add(entry.key);
}
}
if (staleIds.isNotEmpty) {
for (final id in staleIds) {
_discoveredHosts.remove(id);
}
_emitHosts();
}
});
return _hostsController.stream;
}
Future<void> _bindListener(List<int> discoveryKey) async {
try {
_listenSocket = await RawDatagramSocket.bind(
InternetAddress.anyIPv4,
discoveryPort,
reuseAddress: true,
reusePort: true,
);
appLogger.d('LanDiscovery: Listening on port $discoveryPort');
_listenSocket!.listen((RawSocketEvent event) {
if (event == RawSocketEvent.read) {
final datagram = _listenSocket?.receive();
if (datagram != null) {
_handleDatagram(datagram, discoveryKey);
}
}
});
} catch (e) {
appLogger.e('LanDiscovery: Failed to bind listener', error: e);
}
}
void _handleDatagram(Datagram datagram, List<int> discoveryKey) {
try {
final packet = utf8.decode(datagram.data);
final json = jsonDecode(packet) as Map<String, dynamic>;
if (json['app'] != 'plezy') return;
final version = json['v'] as int? ?? 0;
final homeHash = json['homeHash'] as String? ?? '';
final name = json['name'] as String? ?? '';
final platform = json['platform'] as String? ?? '';
final clientId = json['clientId'] as String? ?? '';
final port = json['port'] as int? ?? 0;
final ips = (json['ips'] as List<dynamic>?)?.cast<String>() ?? [];
final hmac = json['hmac'] as String? ?? '';
// Verify beacon HMAC
final auth = RemoteAuthService.instance;
if (!auth.verifyBeaconHmac(
receivedHmac: hmac,
discoveryKey: discoveryKey,
version: version,
homeHash: homeHash,
name: name,
platform: platform,
clientId: clientId,
port: port,
ips: ips,
)) {
return; // Invalid HMAC — not from same home or tampered
}
// Verify homeHash matches (check ±1 epoch window)
if (!auth.matchesDiscoveryTag(homeHash, discoveryKey)) {
return; // Different home
}
// Valid beacon from same home
if (_discoveredHosts.containsKey(clientId)) {
final existing = _discoveredHosts[clientId]!;
existing.lastSeen = DateTime.now();
// Only emit if fields actually changed
if (existing.name != name || existing.port != port) {
_discoveredHosts[clientId] = DiscoveredHost(
clientId: clientId,
name: name,
platform: platform,
port: port,
ips: ips,
);
_emitHosts();
}
} else {
_discoveredHosts[clientId] = DiscoveredHost(
clientId: clientId,
name: name,
platform: platform,
port: port,
ips: ips,
);
appLogger.d('LanDiscovery: Discovered host: $name ($platform) at ${ips.join(", ")}:$port');
_emitHosts();
}
} catch (e) {
// Ignore malformed packets
}
}
void _emitHosts() {
_hostsController.add(_discoveredHosts.values.toList());
}
void stopListening() {
_stopListeningInternal();
_discoveredHosts.clear();
_emitHosts();
}
void _stopListeningInternal() {
_staleCleanupTimer?.cancel();
_staleCleanupTimer = null;
_listenSocket?.close();
_listenSocket = null;
appLogger.d('LanDiscovery: Listening stopped');
}
void dispose() {
stopBroadcasting();
stopListening();
_hostsController.close();
}
}
@@ -0,0 +1,384 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:cryptography/cryptography.dart';
import '../../models/plex_home.dart';
import '../../utils/app_logger.dart';
/// Cryptographic authentication service for companion remote.
///
/// Proves same-home membership via a shared secret derived from Plex home data.
/// This authenticates **group membership**, not individual user identity —
/// any device in the same Plex home can connect.
class RemoteAuthService {
RemoteAuthService._();
static final instance = RemoteAuthService._();
// Reusable crypto algorithm instances
static final _hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32);
static final _aesGcm = AesGcm.with256bits();
// Cached home secret — derived in memory, never persisted
List<int>? _cachedHomeSecret;
int? _cachedHomeId;
String? _cachedAdminUUID;
/// Build canonical IKM bytes from home data.
/// Format: [4-byte BE len][utf8 bytes] for each field, in fixed order.
static Uint8List _canonicalIkm(int homeId, String adminUUID) {
final homeIdBytes = utf8.encode(homeId.toString());
final uuidBytes = utf8.encode(adminUUID.toLowerCase());
final buf = BytesWriter();
buf.writeUint32BE(homeIdBytes.length);
buf.writeBytes(homeIdBytes);
buf.writeUint32BE(uuidBytes.length);
buf.writeBytes(uuidBytes);
return buf.toBytes();
}
/// Derive the long-term home secret via HKDF-SHA256.
/// This is derived in memory from cached Plex data — never persisted.
Future<List<int>> deriveHomeSecret(int homeId, String adminUUID) async {
// Return cached if inputs unchanged
if (_cachedHomeSecret != null && _cachedHomeId == homeId && _cachedAdminUUID == adminUUID) {
return _cachedHomeSecret!;
}
final hkdf = _hkdf;
final ikm = _canonicalIkm(homeId, adminUUID);
final secretKey = await hkdf.deriveKey(
secretKey: SecretKey(ikm),
nonce: utf8.encode('plezy-remote-v1'),
info: utf8.encode('home-secret'),
);
_cachedHomeSecret = await secretKey.extractBytes();
_cachedHomeId = homeId;
_cachedAdminUUID = adminUUID;
appLogger.d('RemoteAuth: Derived home secret');
return _cachedHomeSecret!;
}
/// Derive the long-term home secret from a PlexHome object.
Future<List<int>> deriveHomeSecretFromHome(PlexHome home) async {
final admin = home.adminUser;
if (admin == null) {
throw StateError('PlexHome has no admin user');
}
return deriveHomeSecret(home.id, admin.uuid);
}
/// Derive per-session encryption key from homeSecret + both nonces.
Future<List<int>> deriveSessionEncKey(
List<int> homeSecret,
List<int> hostNonce,
List<int> clientNonce,
) async {
final hkdf = _hkdf;
final salt = Uint8List(hostNonce.length + clientNonce.length);
salt.setAll(0, hostNonce);
salt.setAll(hostNonce.length, clientNonce);
// First derive session secret
final sessionSecretKey = await hkdf.deriveKey(
secretKey: SecretKey(homeSecret),
nonce: salt,
info: utf8.encode('plezy-session-v1'),
);
final sessionSecret = await sessionSecretKey.extractBytes();
// Then derive encryption key from session secret
final encKeyResult = await hkdf.deriveKey(
secretKey: SecretKey(sessionSecret),
nonce: const <int>[],
info: utf8.encode('encryption'),
);
return encKeyResult.extractBytes();
}
/// Derive discovery key from homeSecret.
Future<List<int>> deriveDiscoveryKey(List<int> homeSecret) async {
final hkdf = _hkdf;
final key = await hkdf.deriveKey(
secretKey: SecretKey(homeSecret),
nonce: const <int>[],
info: utf8.encode('discovery'),
);
return key.extractBytes();
}
/// Compute rotating discovery tag for beacon filtering.
/// Uses 5-minute epoch windows to reduce cross-network tracking.
String computeDiscoveryTag(List<int> discoveryKey, {DateTime? now}) {
final epochSeconds = ((now ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000);
final window = epochSeconds ~/ 300; // 5-minute windows
final msg = utf8.encode('identify|$window');
final hmac = crypto.Hmac(crypto.sha256, discoveryKey);
return hmac.convert(msg).toString();
}
/// Check if a received homeHash matches any of the ±1 epoch windows.
bool matchesDiscoveryTag(String receivedHash, List<int> discoveryKey, {DateTime? now}) {
final epochSeconds = ((now ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000);
final currentWindow = epochSeconds ~/ 300;
for (final window in [currentWindow - 1, currentWindow, currentWindow + 1]) {
final msg = utf8.encode('identify|$window');
final hmac = crypto.Hmac(crypto.sha256, discoveryKey);
final tag = hmac.convert(msg).toString();
if (_constantTimeEquals(tag, receivedHash)) return true;
}
return false;
}
/// Generate 32 cryptographically random bytes.
List<int> generateNonce() {
final random = Random.secure();
return List<int>.generate(32, (_) => random.nextInt(256));
}
/// Compute auth tag (HMAC-SHA256) over the full handshake transcript.
/// Domain-separated with "plezy-auth-v1|" prefix.
String computeAuthTag({
required List<int> homeSecret,
required List<int> hostNonce,
required List<int> clientNonce,
required String hostClientId,
required String userUUID,
required String clientIdentifier,
required String deviceName,
required String platform,
}) {
final msg = _buildAuthTranscript(
hostNonce: hostNonce,
clientNonce: clientNonce,
hostClientId: hostClientId,
userUUID: userUUID,
clientIdentifier: clientIdentifier,
deviceName: deviceName,
platform: platform,
);
final hmac = crypto.Hmac(crypto.sha256, homeSecret);
return hmac.convert(msg).toString();
}
/// Verify auth tag with constant-time comparison.
bool verifyAuthTag({
required String authTag,
required List<int> homeSecret,
required List<int> hostNonce,
required List<int> clientNonce,
required String hostClientId,
required String userUUID,
required String clientIdentifier,
required String deviceName,
required String platform,
}) {
final expected = computeAuthTag(
homeSecret: homeSecret,
hostNonce: hostNonce,
clientNonce: clientNonce,
hostClientId: hostClientId,
userUUID: userUUID,
clientIdentifier: clientIdentifier,
deviceName: deviceName,
platform: platform,
);
return _constantTimeEquals(expected, authTag);
}
/// Build the auth transcript message bytes.
List<int> _buildAuthTranscript({
required List<int> hostNonce,
required List<int> clientNonce,
required String hostClientId,
required String userUUID,
required String clientIdentifier,
required String deviceName,
required String platform,
}) {
final buf = BytesWriter();
buf.writeBytes(utf8.encode('plezy-auth-v1|'));
// Fixed-length nonces (32 bytes each) — no prefix needed
buf.writeBytes(hostNonce);
buf.writeBytes(clientNonce);
// Variable-length strings — length-prefixed to prevent field boundary shifting
_writeLengthPrefixed(buf, utf8.encode(hostClientId));
_writeLengthPrefixed(buf, utf8.encode(userUUID));
_writeLengthPrefixed(buf, utf8.encode(clientIdentifier));
_writeLengthPrefixed(buf, utf8.encode(deviceName));
_writeLengthPrefixed(buf, utf8.encode(platform));
return buf.toBytes();
}
/// Compute HMAC for discovery beacon over canonical binary layout.
/// Fields in fixed order: v, homeHash, name, platform, clientId, port, ips (sorted, comma-joined).
String computeBeaconHmac({
required List<int> discoveryKey,
required int version,
required String homeHash,
required String name,
required String platform,
required String clientId,
required int port,
required List<String> ips,
}) {
final buf = BytesWriter();
_writeLengthPrefixed(buf, utf8.encode(version.toString()));
_writeLengthPrefixed(buf, utf8.encode(homeHash));
_writeLengthPrefixed(buf, utf8.encode(name));
_writeLengthPrefixed(buf, utf8.encode(platform));
_writeLengthPrefixed(buf, utf8.encode(clientId));
_writeLengthPrefixed(buf, utf8.encode(port.toString()));
final sortedIps = List<String>.from(ips)..sort();
_writeLengthPrefixed(buf, utf8.encode(sortedIps.join(',')));
final hmac = crypto.Hmac(crypto.sha256, discoveryKey);
return hmac.convert(buf.toBytes()).toString();
}
/// Verify a beacon's HMAC.
bool verifyBeaconHmac({
required String receivedHmac,
required List<int> discoveryKey,
required int version,
required String homeHash,
required String name,
required String platform,
required String clientId,
required int port,
required List<String> ips,
}) {
final expected = computeBeaconHmac(
discoveryKey: discoveryKey,
version: version,
homeHash: homeHash,
name: name,
platform: platform,
clientId: clientId,
port: port,
ips: ips,
);
return _constantTimeEquals(expected, receivedHmac);
}
// ── AES-256-GCM Encryption ──
static const int _directionHost = 0x01;
static const int _directionClient = 0x02;
/// Build the 12-byte GCM nonce from direction + counter.
static Uint8List buildNonce(int direction, int counter) {
final nonce = Uint8List(12);
// 4-byte direction (big-endian)
nonce[0] = (direction >> 24) & 0xFF;
nonce[1] = (direction >> 16) & 0xFF;
nonce[2] = (direction >> 8) & 0xFF;
nonce[3] = direction & 0xFF;
// 8-byte counter (big-endian)
nonce[4] = (counter >> 56) & 0xFF;
nonce[5] = (counter >> 48) & 0xFF;
nonce[6] = (counter >> 40) & 0xFF;
nonce[7] = (counter >> 32) & 0xFF;
nonce[8] = (counter >> 24) & 0xFF;
nonce[9] = (counter >> 16) & 0xFF;
nonce[10] = (counter >> 8) & 0xFF;
nonce[11] = counter & 0xFF;
return nonce;
}
/// Encrypt plaintext with AES-256-GCM.
/// Returns ciphertext + auth tag (nonce is implicit from counters).
Future<List<int>> encrypt(
List<int> sessionEncKey,
List<int> plaintext, {
required bool isHost,
required int counter,
}) async {
final algo = _aesGcm;
final nonce = buildNonce(isHost ? _directionHost : _directionClient, counter);
final secretBox = await algo.encrypt(
plaintext,
secretKey: SecretKey(sessionEncKey),
nonce: nonce,
);
// Return ciphertext + mac (nonce is implicit)
return [...secretBox.cipherText, ...secretBox.mac.bytes];
}
/// Decrypt ciphertext + auth tag with AES-256-GCM.
/// Receiver reconstructs the nonce from its own expected counter.
Future<List<int>> decrypt(
List<int> data,
List<int> sessionEncKey, {
required bool fromHost,
required int expectedCounter,
}) async {
final algo = _aesGcm;
final nonce = buildNonce(fromHost ? _directionHost : _directionClient, expectedCounter);
if (data.length < 16) {
throw ArgumentError('Encrypted data too short');
}
final cipherText = data.sublist(0, data.length - 16);
final mac = Mac(data.sublist(data.length - 16));
final secretBox = SecretBox(cipherText, nonce: nonce, mac: mac);
return algo.decrypt(secretBox, secretKey: SecretKey(sessionEncKey));
}
// ── Utility ──
static void _writeLengthPrefixed(BytesWriter buf, List<int> bytes) {
buf.writeUint32BE(bytes.length);
buf.writeBytes(bytes);
}
/// Constant-time string comparison to prevent timing attacks.
static bool _constantTimeEquals(String a, String b) {
if (a.length != b.length) return false;
var result = 0;
for (var i = 0; i < a.length; i++) {
result |= a.codeUnitAt(i) ^ b.codeUnitAt(i);
}
return result == 0;
}
/// Clear cached home secret (e.g. on logout).
void clearCache() {
_cachedHomeSecret = null;
_cachedHomeId = null;
_cachedAdminUUID = null;
}
// Static direction constants for external use
static int get directionHost => _directionHost;
static int get directionClient => _directionClient;
}
/// Helper for building byte arrays.
class BytesWriter {
final _bytes = <int>[];
void writeBytes(List<int> data) => _bytes.addAll(data);
void writeUint32BE(int value) {
_bytes.add((value >> 24) & 0xFF);
_bytes.add((value >> 16) & 0xFF);
_bytes.add((value >> 8) & 0xFF);
_bytes.add(value & 0xFF);
}
Uint8List toBytes() => Uint8List.fromList(_bytes);
}
+12
View File
@@ -92,6 +92,7 @@ class SettingsService extends BaseSharedPreferencesService {
static const String _keyMpvConfigPresets = 'mpv_config_presets'; static const String _keyMpvConfigPresets = 'mpv_config_presets';
static const String _keyMaxVolume = 'max_volume'; static const String _keyMaxVolume = 'max_volume';
static const String _keyEnableDiscordRPC = 'enable_discord_rpc'; static const String _keyEnableDiscordRPC = 'enable_discord_rpc';
static const String _keyEnableCompanionRemoteServer = 'enable_companion_remote_server';
static const String _keyAutoPip = 'auto_pip'; static const String _keyAutoPip = 'auto_pip';
static const String _keyMatchContentFrameRate = 'match_content_frame_rate'; static const String _keyMatchContentFrameRate = 'match_content_frame_rate';
static const String _keyTunneledPlayback = 'tunneled_playback'; static const String _keyTunneledPlayback = 'tunneled_playback';
@@ -1104,6 +1105,17 @@ class SettingsService extends BaseSharedPreferencesService {
return prefs.getBool(_keyEnableDiscordRPC) ?? false; // Default disabled return prefs.getBool(_keyEnableDiscordRPC) ?? false; // Default disabled
} }
// Companion Remote Server
Future<void> setEnableCompanionRemoteServer(bool enabled) async {
await prefs.setBool(_keyEnableCompanionRemoteServer, enabled);
}
bool getEnableCompanionRemoteServer() {
// Default enabled on desktop/TV, disabled on mobile
return prefs.getBool(_keyEnableCompanionRemoteServer) ??
(Platform.isMacOS || Platform.isWindows || Platform.isLinux);
}
// Auto Picture-in-Picture (Android & iOS) // Auto Picture-in-Picture (Android & iOS)
Future<void> setAutoPip(bool enabled) async { Future<void> setAutoPip(bool enabled) async {
await prefs.setBool(_keyAutoPip, enabled); await prefs.setBool(_keyAutoPip, enabled);
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../providers/companion_remote_provider.dart'; import '../../providers/companion_remote_provider.dart';
import '../../providers/user_profile_provider.dart';
import '../../focus/focusable_button.dart'; import '../../focus/focusable_button.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
@@ -24,58 +23,55 @@ class RemoteSessionDialog extends StatefulWidget {
} }
class _RemoteSessionDialogState extends State<RemoteSessionDialog> { class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
bool _isCreatingSession = false; bool _isStarting = false;
String? _errorMessage; String? _errorMessage;
List<String>? _hostAddresses; // Each format: "ip:port"
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Defer to avoid notifyListeners() during build phase WidgetsBinding.instance.addPostFrameCallback((_) => _ensureServerRunning());
WidgetsBinding.instance.addPostFrameCallback((_) => _createSession());
} }
Future<void> _createSession() async { Future<void> _ensureServerRunning() async {
final provider = context.read<CompanionRemoteProvider>();
if (provider.isHostServerRunning) return;
setState(() { setState(() {
_isCreatingSession = true; _isStarting = true;
_errorMessage = null; _errorMessage = null;
_hostAddresses = null;
}); });
try { try {
final provider = context.read<CompanionRemoteProvider>(); final home = context.read<UserProfileProvider>().home;
final result = await provider.createSession(); await provider.ensureCryptoReady(home);
if (!mounted) return;
await provider.startHostServer();
if (!mounted) return; if (mounted) setState(() => _isStarting = false);
setState(() {
_isCreatingSession = false;
_hostAddresses = result.addresses;
});
} catch (e) { } catch (e) {
appLogger.e('Failed to create companion remote session', error: e); appLogger.e('Failed to start companion remote server', error: e);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_isCreatingSession = false; _isStarting = false;
_errorMessage = e.toString(); _errorMessage = e.toString();
}); });
} }
} }
void _copyToClipboard(String text, String label) { Future<void> _toggleServer() async {
Clipboard.setData(ClipboardData(text: text)); final provider = context.read<CompanionRemoteProvider>();
ScaffoldMessenger.of(context).showSnackBar( if (provider.isHostServerRunning) {
SnackBar( await provider.stopHostServer();
content: Text(t.companionRemote.session.copiedToClipboard(label: label)), } else {
duration: const Duration(seconds: 2), await _ensureServerRunning();
), }
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<CompanionRemoteProvider>( return Consumer<CompanionRemoteProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
if (_isCreatingSession) { if (_isStarting) {
return Dialog( return Dialog(
child: Padding( child: Padding(
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
@@ -84,7 +80,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
const SizedBox(height: 16), const SizedBox(height: 16),
Text(t.companionRemote.session.creatingSession, style: Theme.of(context).textTheme.titleMedium), Text(t.companionRemote.session.startingServer, style: Theme.of(context).textTheme.titleMedium),
], ],
), ),
), ),
@@ -105,27 +101,11 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(t.common.close)), TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(t.common.close)),
TextButton(onPressed: _createSession, child: Text(t.common.retry)), TextButton(onPressed: _ensureServerRunning, child: Text(t.common.retry)),
], ],
); );
} }
final session = provider.session;
if (session == null || _hostAddresses == null || _hostAddresses!.isEmpty) {
return AlertDialog(
title: Text(t.common.error),
content: Text(t.companionRemote.session.noSession),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(t.common.close))],
);
}
// Extract port from first address (all share the same port)
final port = _hostAddresses!.first.split(':').last;
final ips = _hostAddresses!.map((a) => a.split(':').first).join(',');
// URL-wrapped QR format: comma-separated IPs for multi-NIC support
final qrData = 'https://plezy.app/scan#$ips|$port|${session.sessionId}|${session.pin}';
return Dialog( return Dialog(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500), constraints: const BoxConstraints(maxWidth: 500),
@@ -145,16 +125,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
children: [ children: [
Text(t.companionRemote.title, style: Theme.of(context).textTheme.headlineSmall), Text(t.companionRemote.title, style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( _buildStatusLine(context, provider),
session.connectedDevice != null
? t.companionRemote.connectedTo(name: session.connectedDevice!.name)
: t.companionRemote.session.waitingForConnection,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: session.connectedDevice != null
? Colors.green
: Theme.of(context).textTheme.bodySmall?.color,
),
),
], ],
), ),
), ),
@@ -162,113 +133,27 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
if (session.connectedDevice == null) ...[
Text( // Server status card
t.companionRemote.session.scanQrCode, _buildServerStatus(context, provider),
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center, // Connected device info
), if (provider.connectedDevice != null) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
Center( _buildConnectedDevice(context, provider),
child: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.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(
t.companionRemote.session.orEnterManually,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
..._hostAddresses!.map(
(addr) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildCodeCard(
context,
t.companionRemote.session.hostAddress,
addr,
onCopy: () => _copyToClipboard(addr, t.companionRemote.session.hostAddress),
),
),
),
_buildCodeCard(
context,
t.companionRemote.session.sessionId,
session.sessionId,
onCopy: () => _copyToClipboard(session.sessionId, t.companionRemote.session.sessionId),
),
const SizedBox(height: 12),
_buildCodeCard(
context,
t.companionRemote.session.pin,
session.pin,
onCopy: () => _copyToClipboard(session.pin, t.companionRemote.session.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(t.companionRemote.session.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(
t.companionRemote.session.usePhoneToControl,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
], ],
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (session.connectedDevice != null) TextButton.icon(
TextButton.icon( onPressed: _toggleServer,
onPressed: () async { icon: Icon(provider.isHostServerRunning ? Icons.stop : Icons.play_arrow),
await provider.leaveSession(); label: Text(
await _createSession(); provider.isHostServerRunning
}, ? t.companionRemote.session.stopServer
icon: const Icon(Icons.refresh), : t.companionRemote.session.startServer,
label: Text(t.companionRemote.session.newSession),
),
const SizedBox(width: 8),
FocusableButton(
onPressed: () async {
await provider.leaveSession();
if (context.mounted) {
Navigator.of(context).pop();
}
},
child: TextButton(
onPressed: () async {
await provider.leaveSession();
if (context.mounted) {
Navigator.of(context).pop();
}
},
child: Text(t.common.disconnect),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -290,29 +175,88 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
); );
} }
Widget _buildCodeCard(BuildContext context, String label, String code, {VoidCallback? onCopy}) { Widget _buildStatusLine(BuildContext context, CompanionRemoteProvider provider) {
if (provider.connectedDevice != null) {
return Text(
t.companionRemote.connectedTo(name: provider.connectedDevice!.name),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.green),
);
}
if (provider.isHostServerRunning) {
return Text(
t.companionRemote.session.serverRunning,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
);
}
return Text(
t.companionRemote.session.serverStopped,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey),
);
}
Widget _buildServerStatus(BuildContext context, CompanionRemoteProvider provider) {
final isRunning = provider.isHostServerRunning;
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Row( child: Row(
children: [ children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isRunning ? Colors.green : Colors.grey,
),
),
const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, style: Theme.of(context).textTheme.bodySmall), Text(
isRunning
? t.companionRemote.session.serverRunning
: t.companionRemote.session.serverStopped,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
code, isRunning
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontFamily: 'monospace', letterSpacing: 2), ? t.companionRemote.session.serverRunningDescription
: t.companionRemote.session.serverStoppedDescription,
style: Theme.of(context).textTheme.bodySmall,
), ),
], ],
), ),
), ),
IconButton( ],
icon: const Icon(Icons.copy), ),
onPressed: onCopy, ),
tooltip: t.companionRemote.session.copyToClipboard, );
}
Widget _buildConnectedDevice(BuildContext context, CompanionRemoteProvider provider) {
final device = provider.connectedDevice!;
return 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(t.companionRemote.session.connected, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text(device.name, style: Theme.of(context).textTheme.bodyLarge),
Text(device.platform, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 12),
Text(
t.companionRemote.session.usePhoneToControl,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
), ),
], ],
), ),
+1 -9
View File
@@ -286,7 +286,7 @@ packages:
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
cryptography: cryptography:
dependency: transitive dependency: "direct main"
description: description:
name: cryptography name: cryptography
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0" sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
@@ -698,14 +698,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce
url: "https://pub.dev"
source: hosted
version: "7.2.0"
native_toolchain_c: native_toolchain_c:
dependency: transitive dependency: transitive
description: description:
+1 -1
View File
@@ -43,6 +43,7 @@ dependencies:
drift: ^2.31.0 drift: ^2.31.0
sqlite3_flutter_libs: ^0.5.42 sqlite3_flutter_libs: ^0.5.42
crypto: ^3.0.7 crypto: ^3.0.7
cryptography: ^2.9.0
file_picker: ^10.3.10 file_picker: ^10.3.10
saf_util: ^2.0.0 saf_util: ^2.0.0
saf_stream: ^2.0.0 saf_stream: ^2.0.0
@@ -51,7 +52,6 @@ dependencies:
in_app_review: ^2.0.11 in_app_review: ^2.0.11
dart_discord_presence: ^1.2.0 dart_discord_presence: ^1.2.0
flutter_svg: ^2.2.3 flutter_svg: ^2.2.3
mobile_scanner: ^7.2.0
background_downloader: ^9.5.2 background_downloader: ^9.5.2
sentry_flutter: sentry_flutter:
git: git: