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",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Tillad mobilenheder på dit netværk at styre denne app"
},
"search": {
"hint": "Søg film, serier, musik...",
@@ -778,48 +780,36 @@
"controlDesktop": "Styr en desktopenhed",
"connectedTo": "Forbundet til ${name}",
"session": {
"creatingSession": "Opretter fjernsession...",
"failedToCreate": "Kunne ikke oprette fjernsession:",
"noSession": "Ingen session tilgængelig",
"scanQrCode": "Scan QR-kode",
"orEnterManually": "Eller indtast manuelt",
"hostAddress": "Værtadresse",
"sessionId": "Sessions-ID",
"pin": "PIN",
"startingServer": "Starter fjernserver...",
"failedToCreate": "Kunne ikke starte fjernserver:",
"hostAddress": "Værtsadresse",
"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",
"copiedToClipboard": "${label} kopieret til udklipsholder",
"copyToClipboard": "Kopiér til udklipsholder",
"newSession": "Ny session",
"startServer": "Start server",
"stopServer": "Stop server",
"minimize": "Minimér"
},
"pairing": {
"scan": "Scan",
"manual": "Manuel",
"pairWithDesktop": "Par med desktop",
"enterSessionDetails": "Indtast sessionsdetaljerne vist på din desktopenhed",
"pairWithDesktop": "Opret forbindelse til computer",
"discoveryDescription": "Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Indtast 8-tegns sessions-ID",
"pinHint": "Indtast 6-cifret PIN",
"connecting": "Forbinder...",
"tips": "Tips",
"tipDesktop": "Åbn Plezy på din desktop og aktivér fjernbetjening fra indstillinger eller menu",
"tipScan": "Brug fanen Scan til hurtigt at parre ved at scanne QR-koden på din desktop",
"tipWifi": "Sørg for, at begge enheder er på det samme WiFi-netværk",
"cameraPermissionRequired": "Kameratilladelse er påkrævet for at scanne QR-koder.\nGiv kameraadgang i enhedsindstillinger.",
"cameraError": "Kunne ikke starte kamera: ${error}",
"scanInstruction": "Ret kameraet mod QR-koden vist på din desktop",
"invalidQrCode": "Ugyldigt QR-kodeformat",
"validationHostRequired": "Indtast værtadresse",
"connecting": "Opretter forbindelse...",
"searchingForDevices": "Søger efter enheder...",
"noDevicesFound": "Ingen enheder fundet på dit netværk",
"noDevicesHint": "Sørg for, at Plezy er åben på din computer, og at begge enheder er på det samme WiFi-netværk",
"availableDevices": "Tilgængelige enheder",
"manualConnection": "Manuel forbindelse",
"cryptoInitFailed": "Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.",
"validationHostRequired": "Angiv venligst værtsadresse",
"validationHostFormat": "Format skal være IP:port (f.eks. 192.168.1.100:48632)",
"validationSessionIdRequired": "Indtast et sessions-ID",
"validationSessionIdLength": "Sessions-ID skal være 8 tegn",
"validationPinRequired": "Indtast en PIN",
"validationPinLength": "PIN skal være 6 cifre",
"connectionTimedOut": "Forbindelsen fik timeout. Tjek sessions-ID og PIN.",
"sessionNotFound": "Kunne ikke finde sessionen. Tjek dine oplysninger.",
"failedToConnect": "Kunne ikke forbinde: ${error}"
"connectionTimedOut": "Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.",
"sessionNotFound": "Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.",
"authFailed": "Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.",
"failedToConnect": "Kunne ikke oprette forbindelse: ${error}"
},
"remote": {
"disconnectConfirm": "Vil du afbryde fra fjernsessionen?",
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Mobilgeräten in deinem Netzwerk erlauben, diese App zu steuern"
},
"search": {
"hint": "Filme, Serien, Musik suchen...",
@@ -778,47 +780,35 @@
"controlDesktop": "Ein Desktop-Gerät steuern",
"connectedTo": "Verbunden mit ${name}",
"session": {
"creatingSession": "Fernsteuerungssitzung wird erstellt...",
"failedToCreate": "Fernsteuerungssitzung konnte nicht erstellt werden:",
"noSession": "Keine Sitzung verfügbar",
"scanQrCode": "QR-Code scannen",
"orEnterManually": "Oder manuell eingeben",
"startingServer": "Remote-Server wird gestartet...",
"failedToCreate": "Remote-Server konnte nicht gestartet werden:",
"hostAddress": "Host-Adresse",
"sessionId": "Sitzungs-ID",
"pin": "PIN",
"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",
"copiedToClipboard": "${label} in Zwischenablage kopiert",
"copyToClipboard": "In Zwischenablage kopieren",
"newSession": "Neue Sitzung",
"startServer": "Server starten",
"stopServer": "Server stoppen",
"minimize": "Minimieren"
},
"pairing": {
"scan": "Scannen",
"manual": "Manuell",
"pairWithDesktop": "Mit Desktop koppeln",
"enterSessionDetails": "Gib die Sitzungsdetails ein, die auf deinem Desktop-Gerät angezeigt werden",
"pairWithDesktop": "Mit Desktop verbinden",
"discoveryDescription": "Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8-stellige Sitzungs-ID eingeben",
"pinHint": "6-stellige PIN eingeben",
"connecting": "Verbinde...",
"tips": "Tipps",
"tipDesktop": "Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü",
"tipScan": "Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln",
"tipWifi": "Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind",
"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",
"connecting": "Verbinden...",
"searchingForDevices": "Suche nach Geräten...",
"noDevicesFound": "Keine Geräte in deinem Netzwerk gefunden",
"noDevicesHint": "Stelle sicher, dass Plezy auf deinem Desktop geöffnet ist und beide Geräte im selben WLAN sind",
"availableDevices": "Verfügbare Geräte",
"manualConnection": "Manuelle Verbindung",
"cryptoInitFailed": "Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.",
"validationHostRequired": "Bitte Host-Adresse eingeben",
"validationHostFormat": "Format muss IP:Port sein (z.B. 192.168.1.100:48632)",
"validationSessionIdRequired": "Bitte Sitzungs-ID eingeben",
"validationSessionIdLength": "Sitzungs-ID muss 8 Zeichen lang sein",
"validationPinRequired": "Bitte PIN eingeben",
"validationPinLength": "PIN muss 6 Ziffern haben",
"connectionTimedOut": "Zeitüberschreitung. Bitte Sitzungs-ID und PIN überprüfen.",
"sessionNotFound": "Sitzung nicht gefunden. Bitte Zugangsdaten überprüfen.",
"connectionTimedOut": "Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.",
"sessionNotFound": "Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.",
"authFailed": "Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.",
"failedToConnect": "Verbindung fehlgeschlagen: ${error}"
},
"remote": {
+21 -31
View File
@@ -209,6 +209,8 @@
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"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",
"autoPipDescription": "Automatically enter picture-in-picture when leaving the app during playback",
"matchContentFrameRate": "Match Content Frame Rate",
@@ -778,47 +780,35 @@
"controlDesktop": "Control a desktop device",
"connectedTo": "Connected to ${name}",
"session": {
"creatingSession": "Creating remote session...",
"failedToCreate": "Failed to create remote session:",
"noSession": "No session available",
"scanQrCode": "Scan QR Code",
"orEnterManually": "Or enter manually",
"startingServer": "Starting remote server...",
"failedToCreate": "Failed to start remote server:",
"hostAddress": "Host Address",
"sessionId": "Session ID",
"pin": "PIN",
"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",
"copiedToClipboard": "${label} copied to clipboard",
"copyToClipboard": "Copy to clipboard",
"newSession": "New Session",
"startServer": "Start Server",
"stopServer": "Stop Server",
"minimize": "Minimize"
},
"pairing": {
"scan": "Scan",
"manual": "Manual",
"pairWithDesktop": "Pair with Desktop",
"enterSessionDetails": "Enter the session details shown on your desktop device",
"pairWithDesktop": "Connect to Desktop",
"discoveryDescription": "Devices on your network running Plezy with the same Plex account will appear automatically",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Enter 8-character session ID",
"pinHint": "Enter 6-digit PIN",
"connecting": "Connecting...",
"tips": "Tips",
"tipDesktop": "Open Plezy on your desktop and enable Companion Remote from settings or menu",
"tipScan": "Use the Scan tab to quickly pair by scanning the QR code on your desktop",
"tipWifi": "Make sure both devices are on the same WiFi network",
"cameraPermissionRequired": "Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.",
"cameraError": "Could not start camera: ${error}",
"scanInstruction": "Point your camera at the QR code shown on your desktop",
"invalidQrCode": "Invalid QR code format",
"searchingForDevices": "Looking for devices...",
"noDevicesFound": "No devices found on your network",
"noDevicesHint": "Make sure Plezy is open on your desktop and both devices are on the same WiFi network",
"availableDevices": "Available Devices",
"manualConnection": "Manual Connection",
"cryptoInitFailed": "Could not initialize secure connection. Make sure you are signed in to a Plex account.",
"validationHostRequired": "Please enter host address",
"validationHostFormat": "Format must be IP:port (e.g., 192.168.1.100:48632)",
"validationSessionIdRequired": "Please enter a session ID",
"validationSessionIdLength": "Session ID must be 8 characters",
"validationPinRequired": "Please enter a PIN",
"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.",
"connectionTimedOut": "Connection timed out. Make sure both devices are on the same network.",
"sessionNotFound": "Could not find the device. Make sure Plezy is running on the host.",
"authFailed": "Authentication failed. Make sure both devices are on the same Plex account.",
"failedToConnect": "Failed to connect: ${error}"
},
"remote": {
+25 -35
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"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": {
"hint": "Buscar películas, series, música...",
@@ -778,47 +780,35 @@
"controlDesktop": "Controlar un dispositivo de escritorio",
"connectedTo": "Conectado a ${name}",
"session": {
"creatingSession": "Creando sesión remota...",
"failedToCreate": "Error al crear la sesión remota:",
"noSession": "No hay sesión disponible",
"scanQrCode": "Escanear código QR",
"orEnterManually": "O introducir manualmente",
"startingServer": "Iniciando servidor remoto...",
"failedToCreate": "Error al iniciar el servidor remoto:",
"hostAddress": "Dirección del host",
"sessionId": "ID de sesión",
"pin": "PIN",
"connected": "Conectado",
"waitingForConnection": "Esperando conexión...",
"usePhoneToControl": "Usa tu dispositivo móvil para controlar esta app",
"copiedToClipboard": "${label} copiado al portapapeles",
"copyToClipboard": "Copiar al portapapeles",
"newSession": "Nueva sesión",
"serverRunning": "Servidor remoto activo",
"serverStopped": "Servidor remoto detenido",
"serverRunningDescription": "Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación",
"serverStoppedDescription": "Inicia el servidor para permitir que los dispositivos móviles se conecten",
"usePhoneToControl": "Usa tu dispositivo móvil para controlar esta aplicación",
"startServer": "Iniciar servidor",
"stopServer": "Detener servidor",
"minimize": "Minimizar"
},
"pairing": {
"scan": "Escanear",
"manual": "Manual",
"pairWithDesktop": "Emparejar con escritorio",
"enterSessionDetails": "Introduce los datos de la sesión que aparecen en tu dispositivo de escritorio",
"pairWithDesktop": "Conectar al escritorio",
"discoveryDescription": "Los dispositivos en tu red que ejecutan Plezy con la misma cuenta de Plex aparecerán automáticamente",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Introduce ID de sesión de 8 caracteres",
"pinHint": "Introduce PIN de 6 dígitos",
"connecting": "Conectando...",
"tips": "Consejos",
"tipDesktop": "Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú",
"tipScan": "Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio",
"tipWifi": "Asegúrate de que ambos dispositivos estén en la misma red WiFi",
"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.",
"cameraError": "No se pudo iniciar la cámara: ${error}",
"scanInstruction": "Apunta tu cámara al código QR que aparece en tu escritorio",
"invalidQrCode": "Formato de código QR no válido",
"validationHostRequired": "Por favor, introduce la dirección del host",
"validationHostFormat": "El formato debe ser IP:puerto (ej., 192.168.1.100:48632)",
"validationSessionIdRequired": "Por favor, introduce un ID de sesión",
"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.",
"searchingForDevices": "Buscando dispositivos...",
"noDevicesFound": "No se encontraron dispositivos en tu red",
"noDevicesHint": "Asegúrate de que Plezy esté abierto en tu escritorio y que ambos dispositivos estén en la misma red WiFi",
"availableDevices": "Dispositivos disponibles",
"manualConnection": "Conexión manual",
"cryptoInitFailed": "No se pudo inicializar la conexión segura. Asegúrate de haber iniciado sesión en una cuenta de Plex.",
"validationHostRequired": "Ingresa la dirección del host",
"validationHostFormat": "El formato debe ser IP:puerto (ej. 192.168.1.100:48632)",
"connectionTimedOut": "Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.",
"sessionNotFound": "No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.",
"authFailed": "Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.",
"failedToConnect": "Error al conectar: ${error}"
},
"remote": {
+24 -34
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"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": {
"hint": "Rechercher des films, des séries, de la musique...",
@@ -778,47 +780,35 @@
"controlDesktop": "Contrôler un appareil de bureau",
"connectedTo": "Connecté à ${name}",
"session": {
"creatingSession": "Création de la session distante...",
"failedToCreate": "Échec de la création de la session distante :",
"noSession": "Aucune session disponible",
"scanQrCode": "Scanner le QR Code",
"orEnterManually": "Ou saisir manuellement",
"startingServer": "Démarrage du serveur distant...",
"failedToCreate": "Échec du démarrage du serveur distant :",
"hostAddress": "Adresse de l'hôte",
"sessionId": "ID de session",
"pin": "PIN",
"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",
"copiedToClipboard": "${label} copié dans le presse-papiers",
"copyToClipboard": "Copier dans le presse-papiers",
"newSession": "Nouvelle session",
"startServer": "Démarrer le serveur",
"stopServer": "Arrêter le serveur",
"minimize": "Réduire"
},
"pairing": {
"scan": "Scanner",
"manual": "Manuel",
"pairWithDesktop": "Jumeler avec un bureau",
"enterSessionDetails": "Saisissez les détails de la session affichés sur votre appareil de bureau",
"pairWithDesktop": "Se connecter au bureau",
"discoveryDescription": "Les appareils de votre réseau exécutant Plezy avec le même compte Plex apparaîtront automatiquement",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Saisissez l'ID de session à 8 caractères",
"pinHint": "Saisissez le PIN à 6 chiffres",
"connecting": "Connexion...",
"tips": "Conseils",
"tipDesktop": "Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu",
"tipScan": "Utilisez l'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau",
"tipWifi": "Assurez-vous que les deux appareils sont sur le même réseau WiFi",
"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.",
"cameraError": "Impossible de démarrer la caméra : ${error}",
"scanInstruction": "Pointez votre caméra vers le QR code affiché sur votre bureau",
"invalidQrCode": "Format de QR code invalide",
"validationHostRequired": "Veuillez saisir l'adresse de l'hôte",
"validationHostFormat": "Le format doit être IP:port (ex : 192.168.1.100:48632)",
"validationSessionIdRequired": "Veuillez saisir un ID de session",
"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.",
"searchingForDevices": "Recherche d'appareils...",
"noDevicesFound": "Aucun appareil trouvé sur votre réseau",
"noDevicesHint": "Assurez-vous que Plezy est ouvert sur votre bureau et que les deux appareils sont sur le même réseau WiFi",
"availableDevices": "Appareils disponibles",
"manualConnection": "Connexion manuelle",
"cryptoInitFailed": "Impossible d'initialiser la connexion sécurisée. Assurez-vous d'être connecté à un compte Plex.",
"validationHostRequired": "Veuillez entrer l'adresse de l'hôte",
"validationHostFormat": "Le format doit être IP:port (ex. 192.168.1.100:48632)",
"connectionTimedOut": "Délai de connexion dépassé. Assurez-vous que les deux appareils sont sur le même réseau.",
"sessionNotFound": "Appareil introuvable. Assurez-vous que Plezy est en cours d'exécution sur l'hôte.",
"authFailed": "Échec de l'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.",
"failedToConnect": "Échec de la connexion : ${error}"
},
"remote": {
+22 -32
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "Server telecomando",
"companionRemoteServerDescription": "Consenti ai dispositivi mobili nella tua rete di controllare questa app"
},
"search": {
"hint": "Cerca film. spettacoli, musica...",
@@ -778,47 +780,35 @@
"controlDesktop": "Controlla un dispositivo desktop",
"connectedTo": "Connesso a ${name}",
"session": {
"creatingSession": "Creazione sessione remota...",
"failedToCreate": "Impossibile creare la sessione remota:",
"noSession": "Nessuna sessione disponibile",
"scanQrCode": "Scansiona QR Code",
"orEnterManually": "Oppure inserisci manualmente",
"startingServer": "Avvio del server remoto...",
"failedToCreate": "Impossibile avviare il server remoto:",
"hostAddress": "Indirizzo host",
"sessionId": "ID sessione",
"pin": "PIN",
"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",
"copiedToClipboard": "${label} copiato negli appunti",
"copyToClipboard": "Copia negli appunti",
"newSession": "Nuova sessione",
"startServer": "Avvia server",
"stopServer": "Arresta server",
"minimize": "Riduci"
},
"pairing": {
"scan": "Scansiona",
"manual": "Manuale",
"pairWithDesktop": "Associa con desktop",
"enterSessionDetails": "Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop",
"pairWithDesktop": "Connetti al desktop",
"discoveryDescription": "I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Inserisci ID sessione di 8 caratteri",
"pinHint": "Inserisci PIN di 6 cifre",
"connecting": "Connessione...",
"tips": "Suggerimenti",
"tipDesktop": "Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu",
"tipScan": "Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop",
"tipWifi": "Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi",
"cameraPermissionRequired": "L'autorizzazione della fotocamera è necessaria per scansionare i QR code.\nConcedi l'accesso alla fotocamera nelle impostazioni del dispositivo.",
"cameraError": "Impossibile avviare la fotocamera: ${error}",
"scanInstruction": "Punta la fotocamera verso il QR code mostrato sul tuo desktop",
"invalidQrCode": "Formato QR code non valido",
"searchingForDevices": "Ricerca dispositivi...",
"noDevicesFound": "Nessun dispositivo trovato nella tua rete",
"noDevicesHint": "Assicurati che Plezy sia aperto sul tuo desktop e che entrambi i dispositivi siano sulla stessa rete WiFi",
"availableDevices": "Dispositivi disponibili",
"manualConnection": "Connessione manuale",
"cryptoInitFailed": "Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l'accesso a un account Plex.",
"validationHostRequired": "Inserisci l'indirizzo host",
"validationHostFormat": "Il formato deve essere IP:porta (es. 192.168.1.100:48632)",
"validationSessionIdRequired": "Inserisci un ID sessione",
"validationSessionIdLength": "L'ID sessione deve essere di 8 caratteri",
"validationPinRequired": "Inserisci un PIN",
"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.",
"connectionTimedOut": "Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.",
"sessionNotFound": "Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull'host.",
"authFailed": "Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.",
"failedToConnect": "Connessione fallita: ${error}"
},
"remote": {
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "コンパニオンリモートサーバー",
"companionRemoteServerDescription": "ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする"
},
"search": {
"hint": "映画、番組、音楽を検索...",
@@ -778,47 +780,35 @@
"controlDesktop": "デスクトップデバイスを操作",
"connectedTo": "${name}に接続中",
"session": {
"creatingSession": "リモートセッションを作成中...",
"failedToCreate": "リモートセッションの作成に失敗しました:",
"noSession": "利用可能なセッションがありません",
"scanQrCode": "QRコードをスキャン",
"orEnterManually": "または手動で入力",
"startingServer": "リモートサーバーを起動中...",
"failedToCreate": "リモートサーバーの起動に失敗しました:",
"hostAddress": "ホストアドレス",
"sessionId": "セッションID",
"pin": "PIN",
"connected": "接続済み",
"waitingForConnection": "接続を待機中...",
"usePhoneToControl": "モバイルデバイスでこのアプリを操作",
"copiedToClipboard": "${label}をクリップボードにコピーしました",
"copyToClipboard": "クリップボードにコピー",
"newSession": "新しいセッション",
"serverRunning": "リモートサーバー稼働中",
"serverStopped": "リモートサーバー停止中",
"serverRunningDescription": "ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます",
"serverStoppedDescription": "モバイルデバイスの接続を許可するにはサーバーを起動してください",
"usePhoneToControl": "モバイルデバイスでこのアプリを操作できます",
"startServer": "サーバーを起動",
"stopServer": "サーバーを停止",
"minimize": "最小化"
},
"pairing": {
"scan": "スキャン",
"manual": "手動",
"pairWithDesktop": "デスクトップとペアリング",
"enterSessionDetails": "デスクトップデバイスに表示されたセッション情報を入力",
"pairWithDesktop": "デスクトップに接続",
"discoveryDescription": "同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8文字のセッションIDを入力",
"pinHint": "6桁のPINを入力",
"connecting": "接続中...",
"tips": "ヒント",
"tipDesktop": "デスクトップでPlezyを開き、設定またはメニューからコンパニオンリモートを有効にしてください",
"tipScan": "スキャンタブを使用して、デスクトップのQRコードをスキャンして素早くペアリング",
"tipWifi": "両方のデバイスが同じWiFiネットワークに接続されていることを確認",
"cameraPermissionRequired": "QRコードをスキャンするにはカメラの権限が必要です。\nデバイスの設定でカメラへのアクセスを許可してください。",
"cameraError": "カメラを起動できませんでした: ${error}",
"scanInstruction": "デスクトップに表示されたQRコードにカメラを向けてください",
"invalidQrCode": "無効なQRコード形式",
"searchingForDevices": "デバイスを検索中...",
"noDevicesFound": "ネットワーク上にデバイスが見つかりません",
"noDevicesHint": "デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください",
"availableDevices": "利用可能なデバイス",
"manualConnection": "手動接続",
"cryptoInitFailed": "安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。",
"validationHostRequired": "ホストアドレスを入力してください",
"validationHostFormat": "形式はIP:ポートである必要があります(例: 192.168.1.100:48632",
"validationSessionIdRequired": "セッションIDを入力してください",
"validationSessionIdLength": "セッションIDは8文字である必要があります",
"validationPinRequired": "PINを入力してください",
"validationPinLength": "PINは6桁である必要があります",
"connectionTimedOut": "接続がタイムアウトしました。セッションIDとPINを確認してください。",
"sessionNotFound": "セッションが見つかりませんでした。認証情報を確認してください。",
"connectionTimedOut": "接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください",
"sessionNotFound": "デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。",
"authFailed": "認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください",
"failedToConnect": "接続に失敗しました: ${error}"
},
"remote": {
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "컴패니언 리모트 서버",
"companionRemoteServerDescription": "네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용"
},
"search": {
"hint": "영화, 시리즈, 음악 등을 검색하세요...",
@@ -778,47 +780,35 @@
"controlDesktop": "데스크톱 기기 제어",
"connectedTo": "${name}에 연결됨",
"session": {
"creatingSession": "원격 세션 생성 중...",
"failedToCreate": "원격 세션 생성 실패:",
"noSession": "사용 가능한 세션이 없습니다",
"scanQrCode": "QR 코드 스캔",
"orEnterManually": "또는 수동으로 입력",
"startingServer": "원격 서버 시작 중...",
"failedToCreate": "원격 서버를 시작하지 못했습니다:",
"hostAddress": "호스트 주소",
"sessionId": "세션 ID",
"pin": "PIN",
"connected": "연결됨",
"waitingForConnection": "연결 대기 중...",
"serverRunning": "원격 서버 활성",
"serverStopped": "원격 서버 중지됨",
"serverRunningDescription": "네트워크의 모바일 기기가 이 앱을 검색하고 연결할 수 있습니다",
"serverStoppedDescription": "모바일 기기의 연결을 허용하려면 서버를 시작하세요",
"usePhoneToControl": "모바일 기기로 이 앱을 제어하세요",
"copiedToClipboard": "${label}이(가) 클립보드에 복사되었습니다",
"copyToClipboard": "클립보드에 복사",
"newSession": "새 세션",
"startServer": "서버 시작",
"stopServer": "서버 중지",
"minimize": "최소화"
},
"pairing": {
"scan": "스캔",
"manual": "수동",
"pairWithDesktop": "데스크톱과 페어링",
"enterSessionDetails": "데스크톱 기기에 표시된 세션 정보를 입력하세요",
"pairWithDesktop": "데스크톱에 연결",
"discoveryDescription": "같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8자리 세션 ID 입력",
"pinHint": "6자리 PIN 입력",
"connecting": "연결 중...",
"tips": "팁",
"tipDesktop": "데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요",
"tipScan": "스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다",
"tipWifi": "두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요",
"cameraPermissionRequired": "QR 코드를 스캔하려면 카메라 권한이 필요합니다.\n기기 설정에서 카메라 접근을 허용해 주세요.",
"cameraError": "카메라를 시작할 수 없습니다: ${error}",
"scanInstruction": "데스크톱에 표시된 QR 코드에 카메라를 향하세요",
"invalidQrCode": "유효하지 않은 QR 코드 형식",
"searchingForDevices": "기기 검색 중...",
"noDevicesFound": "네트워크에서 기기를 찾을 수 없습니다",
"noDevicesHint": "데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요",
"availableDevices": "사용 가능한 기기",
"manualConnection": "수동 연결",
"cryptoInitFailed": "보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.",
"validationHostRequired": "호스트 주소를 입력하세요",
"validationHostFormat": "IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)",
"validationSessionIdRequired": "세션 ID를 입력하세요",
"validationSessionIdLength": "세션 ID는 8자리여야 합니다",
"validationPinRequired": "PIN을 입력하세요",
"validationPinLength": "PIN은 6자리여야 합니다",
"connectionTimedOut": "연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.",
"sessionNotFound": "세션을 찾을 수 없습니다. 자격 증명을 확인하세요.",
"validationHostFormat": "형식은 IP:포트야 합니다 (예: 192.168.1.100:48632)",
"connectionTimedOut": "연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.",
"sessionNotFound": "기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.",
"authFailed": "인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.",
"failedToConnect": "연결 실패: ${error}"
},
"remote": {
+25 -35
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "Companion Remote-server",
"companionRemoteServerDescription": "Tillat mobilenheter på nettverket ditt å styre denne appen"
},
"search": {
"hint": "Søk i filmer, serier, musikk...",
@@ -778,47 +780,35 @@
"controlDesktop": "Kontroller en stasjonær enhet",
"connectedTo": "Tilkoblet ${name}",
"session": {
"creatingSession": "Oppretter fjernøkt...",
"failedToCreate": "Kunne ikke opprette fjernøkt:",
"noSession": "Ingen økt tilgjengelig",
"scanQrCode": "Skann QR-kode",
"orEnterManually": "Eller skriv inn manuelt",
"startingServer": "Starter fjernserver...",
"failedToCreate": "Kunne ikke starte fjernserver:",
"hostAddress": "Vertsadresse",
"sessionId": "Økt-ID",
"pin": "PIN",
"connected": "Tilkoblet",
"waitingForConnection": "Venter på tilkobling...",
"usePhoneToControl": "Bruk mobilenheten din til å kontrollere denne appen",
"copiedToClipboard": "${label} kopiert til utklippstavle",
"copyToClipboard": "Kopier til utklippstavle",
"newSession": "Ny økt",
"serverRunning": "Fjernserver aktiv",
"serverStopped": "Fjernserver stoppet",
"serverRunningDescription": "Mobilenheter på nettverket ditt kan oppdage og koble til denne appen",
"serverStoppedDescription": "Start serveren for å la mobilenheter koble til",
"usePhoneToControl": "Bruk mobilenheten din til å styre denne appen",
"startServer": "Start server",
"stopServer": "Stopp server",
"minimize": "Minimer"
},
"pairing": {
"scan": "Skann",
"manual": "Manuell",
"pairWithDesktop": "Koble til stasjonær",
"enterSessionDetails": "Skriv inn øktdetaljene som vises på den stasjonære enheten din",
"pairWithDesktop": "Koble til datamaskin",
"discoveryDescription": "Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Skriv inn 8-tegns økt-ID",
"pinHint": "Skriv inn 6-sifret PIN",
"connecting": "Kobler til...",
"tips": "Tips",
"tipDesktop": "Åpne Plezy på datamaskinen din og aktiver Følgesvenn-fjernkontroll fra innstillinger eller meny",
"tipScan": "Bruk Skann-fanen for å raskt koble til ved å skanne QR-koden på datamaskinen din",
"tipWifi": "Sørg for at begge enhetene er på samme WiFi-nettverk",
"cameraPermissionRequired": "Kameratillatelse kreves for å skanne QR-koder.\nVennligst gi kameratilgang i enhetsinnstillingene.",
"cameraError": "Kunne ikke starte kamera: ${error}",
"scanInstruction": "Pek kameraet mot QR-koden som vises på datamaskinen din",
"invalidQrCode": "Ugyldig QR-kodeformat",
"validationHostRequired": "Vennligst skriv inn vertsadresse",
"validationHostFormat": "Formatet må være IP:port (f.eks. 192.168.1.100:48632)",
"validationSessionIdRequired": "Vennligst skriv inn en økt-ID",
"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.",
"searchingForDevices": "Søker etter enheter...",
"noDevicesFound": "Ingen enheter funnet på nettverket ditt",
"noDevicesHint": "Sørg for at Plezy er åpen på datamaskinen din og at begge enhetene er på samme WiFi-nettverk",
"availableDevices": "Tilgjengelige enheter",
"manualConnection": "Manuell tilkobling",
"cryptoInitFailed": "Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.",
"validationHostRequired": "Vennligst oppgi vertsadresse",
"validationHostFormat": "Format må være IP:port (f.eks. 192.168.1.100:48632)",
"connectionTimedOut": "Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.",
"sessionNotFound": "Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.",
"authFailed": "Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.",
"failedToConnect": "Kunne ikke koble til: ${error}"
},
"remote": {
+24 -34
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"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": {
"hint": "Zoek films, series, muziek...",
@@ -778,48 +780,36 @@
"controlDesktop": "Bedien een desktop-apparaat",
"connectedTo": "Verbonden met ${name}",
"session": {
"creatingSession": "Externe sessie aanmaken...",
"failedToCreate": "Kan externe sessie niet aanmaken:",
"noSession": "Geen sessie beschikbaar",
"scanQrCode": "Scan QR-code",
"orEnterManually": "Of voer handmatig in",
"startingServer": "Externe server starten...",
"failedToCreate": "Kan externe server niet starten:",
"hostAddress": "Hostadres",
"sessionId": "Sessie-ID",
"pin": "PIN",
"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",
"copiedToClipboard": "${label} gekopieerd naar klembord",
"copyToClipboard": "Kopieer naar klembord",
"newSession": "Nieuwe sessie",
"startServer": "Server starten",
"stopServer": "Server stoppen",
"minimize": "Minimaliseren"
},
"pairing": {
"scan": "Scannen",
"manual": "Handmatig",
"pairWithDesktop": "Koppelen met desktop",
"enterSessionDetails": "Voer de sessiegegevens in die op je desktop-apparaat worden getoond",
"pairWithDesktop": "Verbinden met desktop",
"discoveryDescription": "Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Voer 8-tekens sessie-ID in",
"pinHint": "Voer 6-cijferige PIN in",
"connecting": "Verbinden...",
"tips": "Tips",
"tipDesktop": "Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu",
"tipScan": "Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen",
"tipWifi": "Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten",
"cameraPermissionRequired": "Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.",
"cameraError": "Kan camera niet starten: ${error}",
"scanInstruction": "Richt je camera op de QR-code die op je desktop wordt getoond",
"invalidQrCode": "Ongeldig QR-codeformaat",
"validationHostRequired": "Voer een hostadres in",
"searchingForDevices": "Apparaten zoeken...",
"noDevicesFound": "Geen apparaten gevonden op je netwerk",
"noDevicesHint": "Zorg ervoor dat Plezy geopend is op je desktop en dat beide apparaten op hetzelfde WiFi-netwerk zitten",
"availableDevices": "Beschikbare apparaten",
"manualConnection": "Handmatige verbinding",
"cryptoInitFailed": "Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.",
"validationHostRequired": "Voer het hostadres in",
"validationHostFormat": "Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)",
"validationSessionIdRequired": "Voer een sessie-ID in",
"validationSessionIdLength": "Sessie-ID moet 8 tekens zijn",
"validationPinRequired": "Voer een PIN in",
"validationPinLength": "PIN moet 6 cijfers zijn",
"connectionTimedOut": "Verbinding verlopen. Controleer de sessie-ID en PIN.",
"sessionNotFound": "Kan de sessie niet vinden. Controleer je gegevens.",
"failedToConnect": "Verbinden mislukt: ${error}"
"connectionTimedOut": "Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.",
"sessionNotFound": "Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.",
"authFailed": "Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.",
"failedToConnect": "Kan niet verbinden: ${error}"
},
"remote": {
"disconnectConfirm": "Wil je de verbinding met de externe sessie verbreken?",
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "Serwer zdalnego sterowania",
"companionRemoteServerDescription": "Pozwól urządzeniom mobilnym w sieci sterować tą aplikacją"
},
"search": {
"hint": "Szukaj filmów, seriali, muzyki...",
@@ -778,47 +780,35 @@
"controlDesktop": "Steruj urządzeniem desktop",
"connectedTo": "Połączono z ${name}",
"session": {
"creatingSession": "Tworzenie sesji zdalnej...",
"failedToCreate": "Nie udało się utworzyć sesji zdalnej:",
"noSession": "Brak dostępnej sesji",
"scanQrCode": "Zeskanuj kod QR",
"orEnterManually": "Lub wprowadź ręcznie",
"startingServer": "Uruchamianie serwera zdalnego...",
"failedToCreate": "Nie udało się uruchomić serwera zdalnego:",
"hostAddress": "Adres hosta",
"sessionId": "ID sesji",
"pin": "PIN",
"connected": "Połączono",
"waitingForConnection": "Oczekiwanie na połączenie...",
"usePhoneToControl": "Użyj urządzenia mobilnego do sterowania tą aplikacją",
"copiedToClipboard": "${label} skopiowane do schowka",
"copyToClipboard": "Kopiuj do schowka",
"newSession": "Nowa sesja",
"serverRunning": "Serwer zdalny aktywny",
"serverStopped": "Serwer zdalny zatrzymany",
"serverRunningDescription": "Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć",
"serverStoppedDescription": "Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych",
"usePhoneToControl": "Użyj urządzenia mobilnego, aby sterować tą aplikacją",
"startServer": "Uruchom serwer",
"stopServer": "Zatrzymaj serwer",
"minimize": "Minimalizuj"
},
"pairing": {
"scan": "Skanuj",
"manual": "Ręcznie",
"pairWithDesktop": "Sparuj z komputerem",
"enterSessionDetails": "Wprowadź dane sesji wyświetlone na urządzeniu desktop",
"pairWithDesktop": "Połącz z komputerem",
"discoveryDescription": "Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Wprowadź 8-znakowe ID sesji",
"pinHint": "Wprowadź 6-cyfrowy PIN",
"connecting": "Łączenie...",
"tips": "Wskazówki",
"tipDesktop": "Otwórz Plezy na komputerze i włącz Pilota zdalnego w ustawieniach lub menu",
"tipScan": "Użyj zakładki Skanuj, aby szybko sparować skanując kod QR na komputerze",
"tipWifi": "Upewnij się, że oba urządzenia są w tej samej sieci WiFi",
"cameraPermissionRequired": "Uprawnienie do kamery jest wymagane do skanowania kodów QR.\nPrzyznaj dostęp do kamery w ustawieniach urządzenia.",
"cameraError": "Nie udało się uruchomić kamery: ${error}",
"scanInstruction": "Skieruj kamerę na kod QR wyświetlony na komputerze",
"invalidQrCode": "Nieprawidłowy format kodu QR",
"searchingForDevices": "Szukanie urządzeń...",
"noDevicesFound": "Nie znaleziono urządzeń w sieci",
"noDevicesHint": "Upewnij się, że Plezy jest otwarte na komputerze i oba urządzenia są w tej samej sieci WiFi",
"availableDevices": "Dostępne urządzenia",
"manualConnection": "Połączenie ręczne",
"cryptoInitFailed": "Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.",
"validationHostRequired": "Wprowadź adres hosta",
"validationHostFormat": "Format musi być IP:port (np. 192.168.1.100:48632)",
"validationSessionIdRequired": "Wprowadź ID sesji",
"validationSessionIdLength": "ID sesji musi mieć 8 znaków",
"validationPinRequired": "Wprowadź PIN",
"validationPinLength": "PIN musi mieć 6 cyfr",
"connectionTimedOut": "Upłynął czas połączenia. Sprawdź ID sesji i PIN.",
"sessionNotFound": "Nie znaleziono sesji. Sprawdź swoje dane.",
"connectionTimedOut": "Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.",
"sessionNotFound": "Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.",
"authFailed": "Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.",
"failedToConnect": "Nie udało się połączyć: ${error}"
},
"remote": {
+27 -37
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"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": {
"hint": "Buscar filmes, séries, músicas...",
@@ -778,47 +780,35 @@
"controlDesktop": "Controlar um dispositivo desktop",
"connectedTo": "Conectado a ${name}",
"session": {
"creatingSession": "Criando sessão remota...",
"failedToCreate": "Falha ao criar sessão remota:",
"noSession": "Nenhuma sessão disponível",
"scanQrCode": "Escanear QR Code",
"orEnterManually": "Ou insira manualmente",
"hostAddress": "Endereço do Host",
"sessionId": "ID da Sessão",
"pin": "PIN",
"startingServer": "A iniciar servidor remoto...",
"failedToCreate": "Falha ao iniciar o servidor remoto:",
"hostAddress": "Endereço do host",
"connected": "Conectado",
"waitingForConnection": "Aguardando conexão...",
"usePhoneToControl": "Use seu dispositivo móvel para controlar este app",
"copiedToClipboard": "${label} copiado para a área de transferência",
"copyToClipboard": "Copiar para área de transferência",
"newSession": "Nova Sessão",
"serverRunning": "Servidor remoto ativo",
"serverStopped": "Servidor remoto parado",
"serverRunningDescription": "Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação",
"serverStoppedDescription": "Inicie o servidor para permitir que dispositivos móveis se conectem",
"usePhoneToControl": "Use o seu dispositivo móvel para controlar esta aplicação",
"startServer": "Iniciar servidor",
"stopServer": "Parar servidor",
"minimize": "Minimizar"
},
"pairing": {
"scan": "Escanear",
"manual": "Manual",
"pairWithDesktop": "Parear com Desktop",
"enterSessionDetails": "Insira os detalhes da sessão exibidos no seu dispositivo desktop",
"pairWithDesktop": "Conectar ao desktop",
"discoveryDescription": "Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Insira o ID da sessão de 8 caracteres",
"pinHint": "Insira o PIN de 6 dígitos",
"connecting": "Conectando...",
"tips": "Dicas",
"tipDesktop": "Abra o Plezy no desktop e ative o Controle Remoto nas configurações ou menu",
"tipScan": "Use a aba Escanear para parear rapidamente escaneando o QR code no seu desktop",
"tipWifi": "Certifique-se de que ambos os dispositivos estão na mesma rede WiFi",
"cameraPermissionRequired": "A permissão da câmera é necessária para escanear QR codes.\nConceda acesso à câmera nas configurações do dispositivo.",
"cameraError": "Não foi possível iniciar a câmera: ${error}",
"scanInstruction": "Aponte sua câmera para o QR code exibido no seu desktop",
"invalidQrCode": "Formato de QR code inválido",
"validationHostRequired": "Insira o endereço do host",
"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.",
"connecting": "A conectar...",
"searchingForDevices": "A procurar dispositivos...",
"noDevicesFound": "Nenhum dispositivo encontrado na sua rede",
"noDevicesHint": "Certifique-se de que o Plezy está aberto no seu desktop e que ambos os dispositivos estão na mesma rede WiFi",
"availableDevices": "Dispositivos disponíveis",
"manualConnection": "Conexão manual",
"cryptoInitFailed": "Não foi possível inicializar a conexão segura. Certifique-se de que está conectado a uma conta Plex.",
"validationHostRequired": "Introduza o endereço do host",
"validationHostFormat": "O formato deve ser IP:porta (ex. 192.168.1.100:48632)",
"connectionTimedOut": "Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.",
"sessionNotFound": "Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.",
"authFailed": "Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.",
"failedToConnect": "Falha ao conectar: ${error}"
},
"remote": {
+22 -32
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "Сервер удалённого управления",
"companionRemoteServerDescription": "Разрешить мобильным устройствам в сети управлять этим приложением"
},
"search": {
"hint": "Поиск фильмов, сериалов, музыки...",
@@ -778,47 +780,35 @@
"controlDesktop": "Управлять десктопным устройством",
"connectedTo": "Подключено к ${name}",
"session": {
"creatingSession": "Создание удалённой сессии...",
"failedToCreate": "Не удалось создать удалённую сессию:",
"noSession": "Нет доступных сессий",
"scanQrCode": "Сканировать QR-код",
"orEnterManually": "Или введите вручную",
"startingServer": "Запуск удалённого сервера...",
"failedToCreate": "Не удалось запустить удалённый сервер:",
"hostAddress": "Адрес хоста",
"sessionId": "ID сессии",
"pin": "PIN",
"connected": "Подключено",
"waitingForConnection": "Ожидание подключения...",
"serverRunning": "Удалённый сервер активен",
"serverStopped": "Удалённый сервер остановлен",
"serverRunningDescription": "Мобильные устройства в вашей сети могут обнаруживать это приложение и подключаться к нему",
"serverStoppedDescription": "Запустите сервер, чтобы разрешить подключение мобильных устройств",
"usePhoneToControl": "Используйте мобильное устройство для управления этим приложением",
"copiedToClipboard": "${label} скопировано в буфер обмена",
"copyToClipboard": "Скопировать в буфер обмена",
"newSession": "Новая сессия",
"startServer": "Запустить сервер",
"stopServer": "Остановить сервер",
"minimize": "Свернуть"
},
"pairing": {
"scan": "Сканировать",
"manual": "Вручную",
"pairWithDesktop": "Сопряжение с ПК",
"enterSessionDetails": "Введите данные сессии, показанные на десктопном устройстве",
"pairWithDesktop": "Подключиться к компьютеру",
"discoveryDescription": "Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Введите 8-символьный ID сессии",
"pinHint": "Введите 6-значный PIN",
"connecting": "Подключение...",
"tips": "Советы",
"tipDesktop": "Откройте Plezy на десктопе и включите пульт управления в настройках или меню",
"tipScan": "Используйте вкладку «Сканировать» для быстрого сопряжения через QR-код на десктопе",
"tipWifi": "Убедитесь, что оба устройства находятся в одной WiFi-сети",
"cameraPermissionRequired": "Для сканирования QR-кодов требуется разрешение на камеру.\nПредоставьте доступ к камере в настройках устройства.",
"cameraError": "Не удалось запустить камеру: ${error}",
"scanInstruction": "Наведите камеру на QR-код, показанный на десктопе",
"invalidQrCode": "Недопустимый формат QR-кода",
"searchingForDevices": "Поиск устройств...",
"noDevicesFound": "Устройства в вашей сети не найдены",
"noDevicesHint": "Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi",
"availableDevices": "Доступные устройства",
"manualConnection": "Ручное подключение",
"cryptoInitFailed": "Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.",
"validationHostRequired": "Введите адрес хоста",
"validationHostFormat": "Формат должен быть IP:порт (например, 192.168.1.100:48632)",
"validationSessionIdRequired": "Введите ID сессии",
"validationSessionIdLength": "ID сессии должен содержать 8 символов",
"validationPinRequired": "Введите PIN",
"validationPinLength": "PIN должен содержать 6 цифр",
"connectionTimedOut": "Время подключения истекло. Проверьте ID сессии и PIN.",
"sessionNotFound": "Сессия не найдена. Проверьте ваши учётные данные.",
"connectionTimedOut": "Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.",
"sessionNotFound": "Устройство не найдено. Убедитесь, что Plezy запущен на хосте.",
"authFailed": "Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.",
"failedToConnect": "Не удалось подключиться: ${error}"
},
"remote": {
+2 -2
View File
@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// 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
// 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 seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionDa implements TranslationsCompanionRemo
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Opretter fjernsession...';
@override String get failedToCreate => 'Kunne ikke oprette fjernsession:';
@override String get noSession => 'Ingen session tilgængelig';
@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 startingServer => 'Starter fjernserver...';
@override String get failedToCreate => 'Kunne ikke starte fjernserver:';
@override String get hostAddress => 'Værtsadresse';
@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 copiedToClipboard({required Object label}) => '${label} kopieret til udklipsholder';
@override String get copyToClipboard => 'Kopiér til udklipsholder';
@override String get newSession => 'Ny session';
@override String get startServer => 'Start server';
@override String get stopServer => 'Stop server';
@override String get minimize => 'Minimér';
}
@@ -1298,31 +1297,22 @@ class _TranslationsCompanionRemotePairingDa implements TranslationsCompanionRemo
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get scan => 'Scan';
@override String get manual => 'Manuel';
@override String get pairWithDesktop => 'Par med desktop';
@override String get enterSessionDetails => 'Indtast sessionsdetaljerne vist på din desktopenhed';
@override String get pairWithDesktop => 'Opret forbindelse til computer';
@override String get discoveryDescription => 'Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk';
@override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => 'Indtast 8-tegns sessions-ID';
@override String get pinHint => 'Indtast 6-cifret PIN';
@override String get connecting => 'Forbinder...';
@override String get tips => 'Tips';
@override String get tipDesktop => 'Åbn Plezy på din desktop og aktivér fjernbetjening fra indstillinger eller menu';
@override String get tipScan => 'Brug fanen Scan til hurtigt at parre ved at scanne QR-koden på din desktop';
@override String get tipWifi => 'Sørg for, at begge enheder er på det samme WiFi-netværk';
@override String get cameraPermissionRequired => 'Kameratilladelse er påkrævet for at scanne QR-koder.\nGiv kameraadgang i enhedsindstillinger.';
@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 connecting => 'Opretter forbindelse...';
@override String get searchingForDevices => 'Søger efter enheder...';
@override String get noDevicesFound => 'Ingen enheder fundet på dit netværk';
@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 availableDevices => 'Tilgængelige enheder';
@override String get manualConnection => 'Manuel forbindelse';
@override String get cryptoInitFailed => 'Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.';
@override String get validationHostRequired => 'Angiv venligst værtsadresse';
@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 validationSessionIdLength => 'Sessions-ID skal være 8 tegn';
@override String get validationPinRequired => 'Indtast en PIN';
@override String get validationPinLength => 'PIN skal være 6 cifre';
@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}';
@override String get connectionTimedOut => 'Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.';
@override String get sessionNotFound => 'Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.';
@override String get authFailed => 'Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.';
@override String failedToConnect({required Object error}) => 'Kunne ikke oprette forbindelse: ${error}';
}
// Path: companionRemote.remote
@@ -1600,6 +1590,8 @@ extension on TranslationsDa {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Prøv en anden søgning',
'search.searchYourMedia' => 'Søg i dine medier',
@@ -1881,10 +1873,10 @@ extension on TranslationsDa {
'libraries.groupings.seasons' => 'Sæsoner',
'libraries.groupings.episodes' => 'Episoder',
'libraries.groupings.folders' => 'Mapper',
'about.title' => 'Om',
'about.openSourceLicenses' => 'Open source-licenser',
_ => null,
} ?? switch (path) {
'about.title' => 'Om',
'about.openSourceLicenses' => 'Open source-licenser',
'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'En smuk Plex-klient til Flutter',
'about.viewLicensesDescription' => 'Se licenser for tredjepartsbiblioteker',
@@ -2076,46 +2068,34 @@ extension on TranslationsDa {
'companionRemote.remoteControl' => 'Fjernbetjening',
'companionRemote.controlDesktop' => 'Styr en desktopenhed',
'companionRemote.connectedTo' => ({required Object name}) => 'Forbundet til ${name}',
'companionRemote.session.creatingSession' => 'Opretter fjernsession...',
'companionRemote.session.failedToCreate' => 'Kunne ikke oprette fjernsession:',
'companionRemote.session.noSession' => 'Ingen session tilgængelig',
'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.startingServer' => 'Starter fjernserver...',
'companionRemote.session.failedToCreate' => 'Kunne ikke starte fjernserver:',
'companionRemote.session.hostAddress' => 'Værtsadresse',
'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.copiedToClipboard' => ({required Object label}) => '${label} kopieret til udklipsholder',
'companionRemote.session.copyToClipboard' => 'Kopiér til udklipsholder',
'companionRemote.session.newSession' => 'Ny session',
'companionRemote.session.startServer' => 'Start server',
'companionRemote.session.stopServer' => 'Stop server',
'companionRemote.session.minimize' => 'Minimér',
'companionRemote.pairing.scan' => 'Scan',
'companionRemote.pairing.manual' => 'Manuel',
'companionRemote.pairing.pairWithDesktop' => 'Par med desktop',
'companionRemote.pairing.enterSessionDetails' => 'Indtast sessionsdetaljerne vist på din desktopenhed',
'companionRemote.pairing.pairWithDesktop' => 'Opret forbindelse til computer',
'companionRemote.pairing.discoveryDescription' => 'Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Indtast 8-tegns sessions-ID',
'companionRemote.pairing.pinHint' => 'Indtast 6-cifret PIN',
'companionRemote.pairing.connecting' => 'Forbinder...',
'companionRemote.pairing.tips' => 'Tips',
'companionRemote.pairing.tipDesktop' => 'Åbn Plezy på din desktop og aktivér fjernbetjening fra indstillinger eller menu',
'companionRemote.pairing.tipScan' => 'Brug fanen Scan til hurtigt at parre ved at scanne QR-koden på din desktop',
'companionRemote.pairing.tipWifi' => 'Sørg for, at begge enheder er på det samme WiFi-netværk',
'companionRemote.pairing.cameraPermissionRequired' => 'Kameratilladelse er påkrævet for at scanne QR-koder.\nGiv kameraadgang i enhedsindstillinger.',
'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.connecting' => 'Opretter forbindelse...',
'companionRemote.pairing.searchingForDevices' => 'Søger efter enheder...',
'companionRemote.pairing.noDevicesFound' => 'Ingen enheder fundet på dit netværk',
'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.availableDevices' => 'Tilgængelige enheder',
'companionRemote.pairing.manualConnection' => 'Manuel forbindelse',
'companionRemote.pairing.cryptoInitFailed' => 'Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.',
'companionRemote.pairing.validationHostRequired' => 'Angiv venligst værtsadresse',
'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.validationSessionIdLength' => 'Sessions-ID skal være 8 tegn',
'companionRemote.pairing.validationPinRequired' => 'Indtast en PIN',
'companionRemote.pairing.validationPinLength' => 'PIN skal være 6 cifre',
'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.pairing.connectionTimedOut' => 'Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.',
'companionRemote.pairing.sessionNotFound' => 'Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.',
'companionRemote.pairing.authFailed' => 'Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke oprette forbindelse: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vil du afbryde fra fjernsessionen?',
'companionRemote.remote.reconnecting' => 'Genopretter forbindelse...',
'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 seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionDe implements TranslationsCompanionRemo
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Fernsteuerungssitzung wird erstellt...';
@override String get failedToCreate => 'Fernsteuerungssitzung konnte nicht erstellt 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 startingServer => 'Remote-Server wird gestartet...';
@override String get failedToCreate => 'Remote-Server konnte nicht gestartet werden:';
@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 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 copiedToClipboard({required Object label}) => '${label} in Zwischenablage kopiert';
@override String get copyToClipboard => 'In Zwischenablage kopieren';
@override String get newSession => 'Neue Sitzung';
@override String get startServer => 'Server starten';
@override String get stopServer => 'Server stoppen';
@override String get minimize => 'Minimieren';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingDe implements TranslationsCompanionRemo
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get scan => 'Scannen';
@override String get manual => 'Manuell';
@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 pairWithDesktop => 'Mit Desktop verbinden';
@override String get discoveryDescription => 'Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch';
@override String get hostAddressHint => '192.168.1.100:48632';
@override String get sessionIdHint => '8-stellige Sitzungs-ID eingeben';
@override String get pinHint => '6-stellige PIN eingeben';
@override String get connecting => 'Verbinde...';
@override String get tips => 'Tipps';
@override String get tipDesktop => 'Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü';
@override String get tipScan => 'Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln';
@override String get tipWifi => 'Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind';
@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 connecting => 'Verbinden...';
@override String get searchingForDevices => 'Suche nach Geräten...';
@override String get noDevicesFound => 'Keine Geräte in deinem Netzwerk gefunden';
@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 availableDevices => 'Verfügbare Geräte';
@override String get manualConnection => 'Manuelle Verbindung';
@override String get cryptoInitFailed => 'Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.';
@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 validationSessionIdRequired => 'Bitte Sitzungs-ID eingeben';
@override String get validationSessionIdLength => 'Sitzungs-ID muss 8 Zeichen lang sein';
@override String get validationPinRequired => 'Bitte PIN eingeben';
@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 get connectionTimedOut => 'Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.';
@override String get sessionNotFound => 'Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.';
@override String get authFailed => 'Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.';
@override String failedToConnect({required Object error}) => 'Verbindung fehlgeschlagen: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsDe {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Anderen Suchbegriff versuchen',
'search.searchYourMedia' => 'In den eigenen Medien suchen',
@@ -1881,10 +1873,10 @@ extension on TranslationsDe {
'libraries.groupings.seasons' => 'Staffeln',
'libraries.groupings.episodes' => 'Episoden',
'libraries.groupings.folders' => 'Ordner',
'about.title' => 'Über',
'about.openSourceLicenses' => 'Open-Source-Lizenzen',
_ => null,
} ?? switch (path) {
'about.title' => 'Über',
'about.openSourceLicenses' => 'Open-Source-Lizenzen',
'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'Ein schöner Plex-Client für Flutter',
'about.viewLicensesDescription' => 'Lizenzen von Drittanbieter-Bibliotheken anzeigen',
@@ -2076,45 +2068,33 @@ extension on TranslationsDe {
'companionRemote.remoteControl' => 'Fernsteuerung',
'companionRemote.controlDesktop' => 'Ein Desktop-Gerät steuern',
'companionRemote.connectedTo' => ({required Object name}) => 'Verbunden mit ${name}',
'companionRemote.session.creatingSession' => 'Fernsteuerungssitzung wird erstellt...',
'companionRemote.session.failedToCreate' => 'Fernsteuerungssitzung konnte nicht erstellt werden:',
'companionRemote.session.noSession' => 'Keine Sitzung verfügbar',
'companionRemote.session.scanQrCode' => 'QR-Code scannen',
'companionRemote.session.orEnterManually' => 'Oder manuell eingeben',
'companionRemote.session.startingServer' => 'Remote-Server wird gestartet...',
'companionRemote.session.failedToCreate' => 'Remote-Server konnte nicht gestartet werden:',
'companionRemote.session.hostAddress' => 'Host-Adresse',
'companionRemote.session.sessionId' => 'Sitzungs-ID',
'companionRemote.session.pin' => 'PIN',
'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.copiedToClipboard' => ({required Object label}) => '${label} in Zwischenablage kopiert',
'companionRemote.session.copyToClipboard' => 'In Zwischenablage kopieren',
'companionRemote.session.newSession' => 'Neue Sitzung',
'companionRemote.session.startServer' => 'Server starten',
'companionRemote.session.stopServer' => 'Server stoppen',
'companionRemote.session.minimize' => 'Minimieren',
'companionRemote.pairing.scan' => 'Scannen',
'companionRemote.pairing.manual' => 'Manuell',
'companionRemote.pairing.pairWithDesktop' => 'Mit Desktop koppeln',
'companionRemote.pairing.enterSessionDetails' => 'Gib die Sitzungsdetails ein, die auf deinem Desktop-Gerät angezeigt werden',
'companionRemote.pairing.pairWithDesktop' => 'Mit Desktop verbinden',
'companionRemote.pairing.discoveryDescription' => 'Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '8-stellige Sitzungs-ID eingeben',
'companionRemote.pairing.pinHint' => '6-stellige PIN eingeben',
'companionRemote.pairing.connecting' => 'Verbinde...',
'companionRemote.pairing.tips' => 'Tipps',
'companionRemote.pairing.tipDesktop' => 'Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü',
'companionRemote.pairing.tipScan' => 'Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln',
'companionRemote.pairing.tipWifi' => 'Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind',
'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.connecting' => 'Verbinden...',
'companionRemote.pairing.searchingForDevices' => 'Suche nach Geräten...',
'companionRemote.pairing.noDevicesFound' => 'Keine Geräte in deinem Netzwerk gefunden',
'companionRemote.pairing.noDevicesHint' => 'Stelle sicher, dass Plezy auf deinem Desktop geöffnet ist und beide Geräte im selben WLAN sind',
'companionRemote.pairing.availableDevices' => 'Verfügbare Geräte',
'companionRemote.pairing.manualConnection' => 'Manuelle Verbindung',
'companionRemote.pairing.cryptoInitFailed' => 'Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.',
'companionRemote.pairing.validationHostRequired' => 'Bitte Host-Adresse eingeben',
'companionRemote.pairing.validationHostFormat' => 'Format muss IP:Port sein (z.B. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Bitte Sitzungs-ID eingeben',
'companionRemote.pairing.validationSessionIdLength' => 'Sitzungs-ID muss 8 Zeichen lang sein',
'companionRemote.pairing.validationPinRequired' => 'Bitte PIN eingeben',
'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.connectionTimedOut' => 'Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.',
'companionRemote.pairing.sessionNotFound' => 'Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.',
'companionRemote.pairing.authFailed' => 'Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Verbindung fehlgeschlagen: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Möchtest du die Verbindung zur Fernsteuerungssitzung trennen?',
'companionRemote.remote.reconnecting' => 'Verbindung wird wiederhergestellt...',
+70 -110
View File
@@ -730,6 +730,12 @@ class TranslationsSettingsEn {
/// en: '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'
String get autoPip => 'Auto Picture-in-Picture';
@@ -2892,47 +2898,38 @@ class TranslationsCompanionRemoteSessionEn {
// Translations
/// en: 'Creating remote session...'
String get creatingSession => 'Creating remote session...';
/// en: 'Starting remote server...'
String get startingServer => 'Starting remote server...';
/// en: 'Failed to create remote session:'
String get failedToCreate => 'Failed to create remote session:';
/// 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: 'Failed to start remote server:'
String get failedToCreate => 'Failed to start remote server:';
/// en: 'Host Address'
String get hostAddress => 'Host Address';
/// en: 'Session ID'
String get sessionId => 'Session ID';
/// en: 'PIN'
String get pin => 'PIN';
/// en: 'Connected'
String get connected => 'Connected';
/// en: 'Waiting for connection...'
String get waitingForConnection => 'Waiting for connection...';
/// en: 'Remote server active'
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'
String get usePhoneToControl => 'Use your mobile device to control this app';
/// en: '${label} copied to clipboard'
String copiedToClipboard({required Object label}) => '${label} copied to clipboard';
/// en: 'Start Server'
String get startServer => 'Start Server';
/// en: 'Copy to clipboard'
String get copyToClipboard => 'Copy to clipboard';
/// en: 'New Session'
String get newSession => 'New Session';
/// en: 'Stop Server'
String get stopServer => 'Stop Server';
/// en: 'Minimize'
String get minimize => 'Minimize';
@@ -2946,53 +2943,35 @@ class TranslationsCompanionRemotePairingEn {
// Translations
/// en: 'Scan'
String get scan => 'Scan';
/// en: 'Connect to Desktop'
String get pairWithDesktop => 'Connect to Desktop';
/// en: 'Manual'
String get manual => 'Manual';
/// 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: 'Devices on your network running Plezy with the same Plex account will appear automatically'
String get discoveryDescription => 'Devices on your network running Plezy with the same Plex account will appear automatically';
/// en: '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...'
String get connecting => 'Connecting...';
/// en: 'Tips'
String get tips => 'Tips';
/// en: 'Looking for devices...'
String get searchingForDevices => 'Looking for devices...';
/// en: 'Open Plezy on your desktop and enable Companion Remote from settings or menu'
String get tipDesktop => 'Open Plezy on your desktop and enable Companion Remote from settings or menu';
/// en: 'No devices found on your network'
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'
String get tipScan => '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 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'
String get tipWifi => 'Make sure both devices are on the same WiFi network';
/// en: 'Available Devices'
String get availableDevices => 'Available Devices';
/// en: 'Camera permission is required to scan QR codes. Please grant camera access in your device settings.'
String get cameraPermissionRequired => 'Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.';
/// en: 'Manual Connection'
String get manualConnection => 'Manual Connection';
/// en: 'Could not start camera: ${error}'
String cameraError({required Object error}) => 'Could not start camera: ${error}';
/// 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: 'Could not initialize secure connection. Make sure you are signed in to a Plex account.'
String get cryptoInitFailed => 'Could not initialize secure connection. Make sure you are signed in to a Plex account.';
/// en: '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)'
String get validationHostFormat => 'Format must be IP:port (e.g., 192.168.1.100:48632)';
/// en: 'Please enter a session ID'
String get validationSessionIdRequired => 'Please enter a session ID';
/// en: 'Connection timed out. Make sure both devices are on the same network.'
String get connectionTimedOut => 'Connection timed out. Make sure both devices are on the same network.';
/// en: 'Session ID must be 8 characters'
String get validationSessionIdLength => 'Session ID must be 8 characters';
/// en: 'Could not find the device. Make sure Plezy is running on the host.'
String get sessionNotFound => 'Could not find the device. Make sure Plezy is running on the host.';
/// en: 'Please enter a PIN'
String get validationPinRequired => 'Please enter a PIN';
/// 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: 'Authentication failed. Make sure both devices are on the same Plex account.'
String get authFailed => 'Authentication failed. Make sure both devices are on the same Plex account.';
/// en: '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.discordRichPresence' => 'Discord Rich Presence',
'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.autoPipDescription' => 'Automatically enter picture-in-picture when leaving the app during playback',
'settings.matchContentFrameRate' => 'Match Content Frame Rate',
@@ -3636,10 +3608,10 @@ extension on Translations {
'libraries.groupings.seasons' => 'Seasons',
'libraries.groupings.episodes' => 'Episodes',
'libraries.groupings.folders' => 'Folders',
'about.title' => 'About',
'about.openSourceLicenses' => 'Open Source Licenses',
_ => null,
} ?? switch (path) {
'about.title' => 'About',
'about.openSourceLicenses' => 'Open Source Licenses',
'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'A beautiful Plex client for Flutter',
'about.viewLicensesDescription' => 'View licenses of third-party libraries',
@@ -3831,45 +3803,33 @@ extension on Translations {
'companionRemote.remoteControl' => 'Remote Control',
'companionRemote.controlDesktop' => 'Control a desktop device',
'companionRemote.connectedTo' => ({required Object name}) => 'Connected to ${name}',
'companionRemote.session.creatingSession' => 'Creating remote session...',
'companionRemote.session.failedToCreate' => 'Failed to create remote session:',
'companionRemote.session.noSession' => 'No session available',
'companionRemote.session.scanQrCode' => 'Scan QR Code',
'companionRemote.session.orEnterManually' => 'Or enter manually',
'companionRemote.session.startingServer' => 'Starting remote server...',
'companionRemote.session.failedToCreate' => 'Failed to start remote server:',
'companionRemote.session.hostAddress' => 'Host Address',
'companionRemote.session.sessionId' => 'Session ID',
'companionRemote.session.pin' => 'PIN',
'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.copiedToClipboard' => ({required Object label}) => '${label} copied to clipboard',
'companionRemote.session.copyToClipboard' => 'Copy to clipboard',
'companionRemote.session.newSession' => 'New Session',
'companionRemote.session.startServer' => 'Start Server',
'companionRemote.session.stopServer' => 'Stop Server',
'companionRemote.session.minimize' => 'Minimize',
'companionRemote.pairing.scan' => 'Scan',
'companionRemote.pairing.manual' => 'Manual',
'companionRemote.pairing.pairWithDesktop' => 'Pair with Desktop',
'companionRemote.pairing.enterSessionDetails' => 'Enter the session details shown on your desktop device',
'companionRemote.pairing.pairWithDesktop' => 'Connect to Desktop',
'companionRemote.pairing.discoveryDescription' => 'Devices on your network running Plezy with the same Plex account will appear automatically',
'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.tips' => 'Tips',
'companionRemote.pairing.tipDesktop' => 'Open Plezy on your desktop and enable Companion Remote from settings or menu',
'companionRemote.pairing.tipScan' => 'Use the Scan tab to quickly pair by scanning the QR code on your desktop',
'companionRemote.pairing.tipWifi' => 'Make sure both devices are on the same WiFi network',
'companionRemote.pairing.cameraPermissionRequired' => 'Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Could not start camera: ${error}',
'companionRemote.pairing.scanInstruction' => 'Point your camera at the QR code shown on your desktop',
'companionRemote.pairing.invalidQrCode' => 'Invalid QR code format',
'companionRemote.pairing.searchingForDevices' => 'Looking for devices...',
'companionRemote.pairing.noDevicesFound' => 'No devices found on your network',
'companionRemote.pairing.noDevicesHint' => 'Make sure Plezy is open on your desktop and both devices are on the same WiFi network',
'companionRemote.pairing.availableDevices' => 'Available Devices',
'companionRemote.pairing.manualConnection' => 'Manual Connection',
'companionRemote.pairing.cryptoInitFailed' => 'Could not initialize secure connection. Make sure you are signed in to a Plex account.',
'companionRemote.pairing.validationHostRequired' => 'Please enter host address',
'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.validationSessionIdLength' => 'Session ID must be 8 characters',
'companionRemote.pairing.validationPinRequired' => 'Please enter a PIN',
'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.connectionTimedOut' => 'Connection timed out. Make sure both devices are on the same network.',
'companionRemote.pairing.sessionNotFound' => 'Could not find the device. Make sure Plezy is running on the host.',
'companionRemote.pairing.authFailed' => 'Authentication failed. Make sure both devices are on the same Plex account.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Failed to connect: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Do you want to disconnect from the remote session?',
'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 seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionEs implements TranslationsCompanionRemo
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Creando sesión remota...';
@override String get failedToCreate => 'Error al crear la sesión remota:';
@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 startingServer => 'Iniciando servidor remoto...';
@override String get failedToCreate => 'Error al iniciar el servidor remoto:';
@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 waitingForConnection => 'Esperando conexión...';
@override String get usePhoneToControl => 'Usa tu dispositivo móvil para controlar esta app';
@override String copiedToClipboard({required Object label}) => '${label} copiado al portapapeles';
@override String get copyToClipboard => 'Copiar al portapapeles';
@override String get newSession => 'Nueva sesión';
@override String get serverRunning => 'Servidor remoto activo';
@override String get serverStopped => 'Servidor remoto detenido';
@override String get serverRunningDescription => 'Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación';
@override String get serverStoppedDescription => 'Inicia el servidor para permitir que los dispositivos móviles se conecten';
@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';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingEs implements TranslationsCompanionRemo
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get scan => 'Escanear';
@override String get manual => 'Manual';
@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 pairWithDesktop => 'Conectar al escritorio';
@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 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 tips => 'Consejos';
@override String get tipDesktop => 'Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú';
@override String get tipScan => 'Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio';
@override String get tipWifi => 'Asegúrate de que ambos dispositivos estén en la misma red WiFi';
@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 cameraError({required Object error}) => 'No se pudo iniciar la cámara: ${error}';
@override String get scanInstruction => 'Apunta tu cámara al código QR que aparece en tu escritorio';
@override String get invalidQrCode => 'Formato de código QR no válido';
@override String get validationHostRequired => 'Por favor, introduce la dirección del host';
@override String get validationHostFormat => 'El formato debe ser IP:puerto (ej., 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Por favor, introduce un ID de sesión';
@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 get searchingForDevices => 'Buscando dispositivos...';
@override String get noDevicesFound => 'No se encontraron dispositivos en tu red';
@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 availableDevices => 'Dispositivos disponibles';
@override String get manualConnection => 'Conexión manual';
@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 validationHostRequired => 'Ingresa la dirección del host';
@override String get validationHostFormat => 'El formato debe ser IP:puerto (ej. 192.168.1.100:48632)';
@override String get connectionTimedOut => 'Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.';
@override String get sessionNotFound => 'No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.';
@override String get authFailed => 'Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.';
@override String failedToConnect({required Object error}) => 'Error al conectar: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsEs {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Prueba con un término de búsqueda diferente',
'search.searchYourMedia' => 'Busca en tu contenido',
@@ -1881,10 +1873,10 @@ extension on TranslationsEs {
'libraries.groupings.seasons' => 'Temporadas',
'libraries.groupings.episodes' => 'Episodios',
'libraries.groupings.folders' => 'Carpetas',
'about.title' => 'Acerca de',
'about.openSourceLicenses' => 'Licencias de Código Abierto',
_ => null,
} ?? switch (path) {
'about.title' => 'Acerca de',
'about.openSourceLicenses' => 'Licencias de Código Abierto',
'about.versionLabel' => ({required Object version}) => 'Versión ${version}',
'about.appDescription' => 'Un cliente de Plex para Flutter',
'about.viewLicensesDescription' => 'Ver licencias de librerías de terceros',
@@ -2076,45 +2068,33 @@ extension on TranslationsEs {
'companionRemote.remoteControl' => 'Control remoto',
'companionRemote.controlDesktop' => 'Controlar un dispositivo de escritorio',
'companionRemote.connectedTo' => ({required Object name}) => 'Conectado a ${name}',
'companionRemote.session.creatingSession' => 'Creando sesión remota...',
'companionRemote.session.failedToCreate' => 'Error al crear la sesión remota:',
'companionRemote.session.noSession' => 'No hay sesión disponible',
'companionRemote.session.scanQrCode' => 'Escanear código QR',
'companionRemote.session.orEnterManually' => 'O introducir manualmente',
'companionRemote.session.startingServer' => 'Iniciando servidor remoto...',
'companionRemote.session.failedToCreate' => 'Error al iniciar el servidor remoto:',
'companionRemote.session.hostAddress' => 'Dirección del host',
'companionRemote.session.sessionId' => 'ID de sesión',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Conectado',
'companionRemote.session.waitingForConnection' => 'Esperando conexión...',
'companionRemote.session.usePhoneToControl' => 'Usa tu dispositivo móvil para controlar esta app',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} copiado al portapapeles',
'companionRemote.session.copyToClipboard' => 'Copiar al portapapeles',
'companionRemote.session.newSession' => 'Nueva sesión',
'companionRemote.session.serverRunning' => 'Servidor remoto activo',
'companionRemote.session.serverStopped' => 'Servidor remoto detenido',
'companionRemote.session.serverRunningDescription' => 'Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación',
'companionRemote.session.serverStoppedDescription' => 'Inicia el servidor para permitir que los dispositivos móviles se conecten',
'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.pairing.scan' => 'Escanear',
'companionRemote.pairing.manual' => 'Manual',
'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.pairWithDesktop' => 'Conectar al escritorio',
'companionRemote.pairing.discoveryDescription' => 'Los dispositivos en tu red que ejecutan Plezy con la misma cuenta de Plex aparecerán automáticamente',
'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.tips' => 'Consejos',
'companionRemote.pairing.tipDesktop' => 'Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú',
'companionRemote.pairing.tipScan' => 'Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio',
'companionRemote.pairing.tipWifi' => 'Asegúrate de que ambos dispositivos estén en la misma red WiFi',
'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.cameraError' => ({required Object error}) => 'No se pudo iniciar la cámara: ${error}',
'companionRemote.pairing.scanInstruction' => 'Apunta tu cámara al código QR que aparece en tu escritorio',
'companionRemote.pairing.invalidQrCode' => 'Formato de código QR no válido',
'companionRemote.pairing.validationHostRequired' => 'Por favor, introduce la dirección del host',
'companionRemote.pairing.validationHostFormat' => 'El formato debe ser IP:puerto (ej., 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Por favor, introduce un ID de sesión',
'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.searchingForDevices' => 'Buscando dispositivos...',
'companionRemote.pairing.noDevicesFound' => 'No se encontraron dispositivos en tu red',
'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.availableDevices' => 'Dispositivos disponibles',
'companionRemote.pairing.manualConnection' => 'Conexión manual',
'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.validationHostRequired' => 'Ingresa la dirección del host',
'companionRemote.pairing.validationHostFormat' => 'El formato debe ser IP:puerto (ej. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.',
'companionRemote.pairing.sessionNotFound' => 'No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.',
'companionRemote.pairing.authFailed' => 'Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Error al conectar: ${error}',
'companionRemote.remote.disconnectConfirm' => '¿Quieres desconectarte de la sesión remota?',
'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 seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionFr implements TranslationsCompanionRemo
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Création de la session distante...';
@override String get failedToCreate => 'Échec de la création de la session distante :';
@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 startingServer => 'Démarrage du serveur distant...';
@override String get failedToCreate => 'Échec du démarrage du serveur distant :';
@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 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 copiedToClipboard({required Object label}) => '${label} copié dans le presse-papiers';
@override String get copyToClipboard => 'Copier dans le presse-papiers';
@override String get newSession => 'Nouvelle session';
@override String get startServer => 'Démarrer le serveur';
@override String get stopServer => 'Arrêter le serveur';
@override String get minimize => 'Réduire';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingFr implements TranslationsCompanionRemo
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get scan => 'Scanner';
@override String get manual => 'Manuel';
@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 pairWithDesktop => 'Se connecter au bureau';
@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 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 tips => 'Conseils';
@override String get tipDesktop => 'Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu';
@override String get tipScan => 'Utilisez l\'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau';
@override String get tipWifi => 'Assurez-vous que les deux appareils sont sur le même réseau WiFi';
@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 cameraError({required Object error}) => 'Impossible de démarrer la caméra : ${error}';
@override String get scanInstruction => 'Pointez votre caméra vers le QR code affiché sur votre bureau';
@override String get invalidQrCode => 'Format de QR code invalide';
@override String get validationHostRequired => 'Veuillez saisir l\'adresse de l\'hôte';
@override String get validationHostFormat => 'Le format doit être IP:port (ex : 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Veuillez saisir un ID de session';
@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 get searchingForDevices => 'Recherche d\'appareils...';
@override String get noDevicesFound => 'Aucun appareil trouvé sur votre réseau';
@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 availableDevices => 'Appareils disponibles';
@override String get manualConnection => 'Connexion manuelle';
@override String get cryptoInitFailed => 'Impossible d\'initialiser la connexion sécurisée. Assurez-vous d\'être connecté à un compte Plex.';
@override String get validationHostRequired => 'Veuillez entrer l\'adresse de l\'hôte';
@override String get validationHostFormat => 'Le format doit être IP:port (ex. 192.168.1.100:48632)';
@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 sessionNotFound => 'Appareil introuvable. Assurez-vous que Plezy est en cours d\'exécution sur l\'hôte.';
@override String get authFailed => 'Échec de l\'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.';
@override String failedToConnect({required Object error}) => 'Échec de la connexion : ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsFr {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Essayez un autre terme de recherche',
'search.searchYourMedia' => 'Rechercher dans vos médias',
@@ -1881,10 +1873,10 @@ extension on TranslationsFr {
'libraries.groupings.seasons' => 'Saisons',
'libraries.groupings.episodes' => 'Épisodes',
'libraries.groupings.folders' => 'Dossiers',
'about.title' => 'À propos',
'about.openSourceLicenses' => 'Licences Open Source',
_ => null,
} ?? switch (path) {
'about.title' => 'À propos',
'about.openSourceLicenses' => 'Licences Open Source',
'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'Un magnifique client Plex pour Flutter',
'about.viewLicensesDescription' => 'Afficher les licences des bibliothèques tierces',
@@ -2076,45 +2068,33 @@ extension on TranslationsFr {
'companionRemote.remoteControl' => 'Télécommande',
'companionRemote.controlDesktop' => 'Contrôler un appareil de bureau',
'companionRemote.connectedTo' => ({required Object name}) => 'Connecté à ${name}',
'companionRemote.session.creatingSession' => 'Création de la session distante...',
'companionRemote.session.failedToCreate' => 'Échec de la création de la session distante :',
'companionRemote.session.noSession' => 'Aucune session disponible',
'companionRemote.session.scanQrCode' => 'Scanner le QR Code',
'companionRemote.session.orEnterManually' => 'Ou saisir manuellement',
'companionRemote.session.startingServer' => 'Démarrage du serveur distant...',
'companionRemote.session.failedToCreate' => 'Échec du démarrage du serveur distant :',
'companionRemote.session.hostAddress' => 'Adresse de l\'hôte',
'companionRemote.session.sessionId' => 'ID de session',
'companionRemote.session.pin' => 'PIN',
'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.copiedToClipboard' => ({required Object label}) => '${label} copié dans le presse-papiers',
'companionRemote.session.copyToClipboard' => 'Copier dans le presse-papiers',
'companionRemote.session.newSession' => 'Nouvelle session',
'companionRemote.session.startServer' => 'Démarrer le serveur',
'companionRemote.session.stopServer' => 'Arrêter le serveur',
'companionRemote.session.minimize' => 'Réduire',
'companionRemote.pairing.scan' => 'Scanner',
'companionRemote.pairing.manual' => 'Manuel',
'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.pairWithDesktop' => 'Se connecter au bureau',
'companionRemote.pairing.discoveryDescription' => 'Les appareils de votre réseau exécutant Plezy avec le même compte Plex apparaîtront automatiquement',
'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.tips' => 'Conseils',
'companionRemote.pairing.tipDesktop' => 'Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu',
'companionRemote.pairing.tipScan' => 'Utilisez l\'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau',
'companionRemote.pairing.tipWifi' => 'Assurez-vous que les deux appareils sont sur le même réseau WiFi',
'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.cameraError' => ({required Object error}) => 'Impossible de démarrer la caméra : ${error}',
'companionRemote.pairing.scanInstruction' => 'Pointez votre caméra vers le QR code affiché sur votre bureau',
'companionRemote.pairing.invalidQrCode' => 'Format de QR code invalide',
'companionRemote.pairing.validationHostRequired' => 'Veuillez saisir l\'adresse de l\'hôte',
'companionRemote.pairing.validationHostFormat' => 'Le format doit être IP:port (ex : 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Veuillez saisir un ID de session',
'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.searchingForDevices' => 'Recherche d\'appareils...',
'companionRemote.pairing.noDevicesFound' => 'Aucun appareil trouvé sur votre réseau',
'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.availableDevices' => 'Appareils disponibles',
'companionRemote.pairing.manualConnection' => 'Connexion manuelle',
'companionRemote.pairing.cryptoInitFailed' => 'Impossible d\'initialiser la connexion sécurisée. Assurez-vous d\'être connecté à un compte Plex.',
'companionRemote.pairing.validationHostRequired' => 'Veuillez entrer l\'adresse de l\'hôte',
'companionRemote.pairing.validationHostFormat' => 'Le format doit être IP:port (ex. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Délai de connexion dépassé. Assurez-vous que les deux appareils sont sur le même réseau.',
'companionRemote.pairing.sessionNotFound' => 'Appareil introuvable. Assurez-vous que Plezy est en cours d\'exécution sur l\'hôte.',
'companionRemote.pairing.authFailed' => 'Échec de l\'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.',
'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.reconnecting' => 'Reconnexion...',
+44 -64
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionIt implements TranslationsCompanionRemo
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Creazione sessione remota...';
@override String get failedToCreate => 'Impossibile creare la sessione remota:';
@override String get noSession => 'Nessuna sessione disponibile';
@override String get scanQrCode => 'Scansiona QR Code';
@override String get orEnterManually => 'Oppure inserisci manualmente';
@override String get startingServer => 'Avvio del server remoto...';
@override String get failedToCreate => 'Impossibile avviare il server remoto:';
@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 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 copiedToClipboard({required Object label}) => '${label} copiato negli appunti';
@override String get copyToClipboard => 'Copia negli appunti';
@override String get newSession => 'Nuova sessione';
@override String get startServer => 'Avvia server';
@override String get stopServer => 'Arresta server';
@override String get minimize => 'Riduci';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingIt implements TranslationsCompanionRemo
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get scan => 'Scansiona';
@override String get manual => 'Manuale';
@override String get pairWithDesktop => 'Associa con desktop';
@override String get enterSessionDetails => 'Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop';
@override String get pairWithDesktop => 'Connetti al desktop';
@override String get discoveryDescription => 'I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente';
@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 tips => 'Suggerimenti';
@override String get tipDesktop => 'Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu';
@override String get tipScan => 'Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop';
@override String get tipWifi => 'Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi';
@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 cameraError({required Object error}) => 'Impossibile avviare la fotocamera: ${error}';
@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 searchingForDevices => 'Ricerca dispositivi...';
@override String get noDevicesFound => 'Nessun dispositivo trovato nella tua rete';
@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 availableDevices => 'Dispositivi disponibili';
@override String get manualConnection => 'Connessione manuale';
@override String get cryptoInitFailed => 'Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l\'accesso a un account Plex.';
@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 validationSessionIdRequired => 'Inserisci un ID sessione';
@override String get validationSessionIdLength => 'L\'ID sessione deve essere di 8 caratteri';
@override String get validationPinRequired => 'Inserisci un PIN';
@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 get connectionTimedOut => 'Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.';
@override String get sessionNotFound => 'Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull\'host.';
@override String get authFailed => 'Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.';
@override String failedToConnect({required Object error}) => 'Connessione fallita: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsIt {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Prova altri termini di ricerca',
'search.searchYourMedia' => 'Cerca nei tuoi media',
@@ -1881,10 +1873,10 @@ extension on TranslationsIt {
'libraries.groupings.seasons' => 'Stagioni',
'libraries.groupings.episodes' => 'Episodi',
'libraries.groupings.folders' => 'Cartelle',
'about.title' => 'Informazioni',
'about.openSourceLicenses' => 'Licenze Open Source',
_ => null,
} ?? switch (path) {
'about.title' => 'Informazioni',
'about.openSourceLicenses' => 'Licenze Open Source',
'about.versionLabel' => ({required Object version}) => 'Versione ${version}',
'about.appDescription' => 'Un bellissimo client Plex per Flutter',
'about.viewLicensesDescription' => 'Visualizza le licenze delle librerie di terze parti',
@@ -2076,45 +2068,33 @@ extension on TranslationsIt {
'companionRemote.remoteControl' => 'Telecomando',
'companionRemote.controlDesktop' => 'Controlla un dispositivo desktop',
'companionRemote.connectedTo' => ({required Object name}) => 'Connesso a ${name}',
'companionRemote.session.creatingSession' => 'Creazione sessione remota...',
'companionRemote.session.failedToCreate' => 'Impossibile creare la sessione remota:',
'companionRemote.session.noSession' => 'Nessuna sessione disponibile',
'companionRemote.session.scanQrCode' => 'Scansiona QR Code',
'companionRemote.session.orEnterManually' => 'Oppure inserisci manualmente',
'companionRemote.session.startingServer' => 'Avvio del server remoto...',
'companionRemote.session.failedToCreate' => 'Impossibile avviare il server remoto:',
'companionRemote.session.hostAddress' => 'Indirizzo host',
'companionRemote.session.sessionId' => 'ID sessione',
'companionRemote.session.pin' => 'PIN',
'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.copiedToClipboard' => ({required Object label}) => '${label} copiato negli appunti',
'companionRemote.session.copyToClipboard' => 'Copia negli appunti',
'companionRemote.session.newSession' => 'Nuova sessione',
'companionRemote.session.startServer' => 'Avvia server',
'companionRemote.session.stopServer' => 'Arresta server',
'companionRemote.session.minimize' => 'Riduci',
'companionRemote.pairing.scan' => 'Scansiona',
'companionRemote.pairing.manual' => 'Manuale',
'companionRemote.pairing.pairWithDesktop' => 'Associa con desktop',
'companionRemote.pairing.enterSessionDetails' => 'Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop',
'companionRemote.pairing.pairWithDesktop' => 'Connetti al desktop',
'companionRemote.pairing.discoveryDescription' => 'I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente',
'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.tips' => 'Suggerimenti',
'companionRemote.pairing.tipDesktop' => 'Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu',
'companionRemote.pairing.tipScan' => 'Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop',
'companionRemote.pairing.tipWifi' => 'Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi',
'companionRemote.pairing.cameraPermissionRequired' => 'L\'autorizzazione della fotocamera è necessaria per scansionare i QR code.\nConcedi l\'accesso alla fotocamera nelle impostazioni del dispositivo.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Impossibile avviare la fotocamera: ${error}',
'companionRemote.pairing.scanInstruction' => 'Punta la fotocamera verso il QR code mostrato sul tuo desktop',
'companionRemote.pairing.invalidQrCode' => 'Formato QR code non valido',
'companionRemote.pairing.searchingForDevices' => 'Ricerca dispositivi...',
'companionRemote.pairing.noDevicesFound' => 'Nessun dispositivo trovato nella tua rete',
'companionRemote.pairing.noDevicesHint' => 'Assicurati che Plezy sia aperto sul tuo desktop e che entrambi i dispositivi siano sulla stessa rete WiFi',
'companionRemote.pairing.availableDevices' => 'Dispositivi disponibili',
'companionRemote.pairing.manualConnection' => 'Connessione manuale',
'companionRemote.pairing.cryptoInitFailed' => 'Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l\'accesso a un account Plex.',
'companionRemote.pairing.validationHostRequired' => 'Inserisci l\'indirizzo host',
'companionRemote.pairing.validationHostFormat' => 'Il formato deve essere IP:porta (es. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Inserisci un ID sessione',
'companionRemote.pairing.validationSessionIdLength' => 'L\'ID sessione deve essere di 8 caratteri',
'companionRemote.pairing.validationPinRequired' => 'Inserisci un PIN',
'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.connectionTimedOut' => 'Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.',
'companionRemote.pairing.sessionNotFound' => 'Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull\'host.',
'companionRemote.pairing.authFailed' => 'Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Connessione fallita: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vuoi disconnetterti dalla sessione remota?',
'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 seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'コンパニオンリモートサーバー';
@override String get companionRemoteServerDescription => 'ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする';
}
// Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionJa implements TranslationsCompanionRemo
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'リモートセッションを作成中...';
@override String get failedToCreate => 'リモートセッションの作成に失敗しました:';
@override String get noSession => '利用可能なセッションがありません';
@override String get scanQrCode => 'QRコードをスキャン';
@override String get orEnterManually => 'または手動で入力';
@override String get startingServer => 'リモートサーバーを起動中...';
@override String get failedToCreate => 'リモートサーバーの起動に失敗しました:';
@override String get hostAddress => 'ホストアドレス';
@override String get sessionId => 'セッションID';
@override String get pin => 'PIN';
@override String get connected => '接続済み';
@override String get waitingForConnection => '接続を待機中...';
@override String get usePhoneToControl => 'モバイルデバイスでこのアプリを操作';
@override String copiedToClipboard({required Object label}) => '${label}をクリップボードにコピーしました';
@override String get copyToClipboard => 'クリップボードにコピー';
@override String get newSession => '新しいセッション';
@override String get serverRunning => 'リモートサーバー稼働中';
@override String get serverStopped => 'リモートサーバー停止中';
@override String get serverRunningDescription => 'ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます';
@override String get serverStoppedDescription => 'モバイルデバイスの接続を許可するにはサーバーを起動してください';
@override String get usePhoneToControl => 'モバイルデバイスでこのアプリを操作できます';
@override String get startServer => 'サーバーを起動';
@override String get stopServer => 'サーバーを停止';
@override String get minimize => '最小化';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingJa implements TranslationsCompanionRemo
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get scan => 'スキャン';
@override String get manual => '手動';
@override String get pairWithDesktop => 'デスクトップとペアリング';
@override String get enterSessionDetails => 'デスクトップデバイスに表示されたセッション情報を入力';
@override String get pairWithDesktop => 'デスクトップに接続';
@override String get discoveryDescription => '同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます';
@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 tips => 'ヒント';
@override String get tipDesktop => 'デスクトップでPlezyを開き、設定またはメニューからコンパニオンリモートを有効にしてください';
@override String get tipScan => 'スキャンタブを使用して、デスクトップのQRコードをスキャンして素早くペアリング';
@override String get tipWifi => '両方のデバイスが同じWiFiネットワークに接続されていることを確認';
@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 searchingForDevices => 'デバイスを検索中...';
@override String get noDevicesFound => 'ネットワーク上にデバイスが見つかりません';
@override String get noDevicesHint => 'デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください';
@override String get availableDevices => '利用可能なデバイス';
@override String get manualConnection => '手動接続';
@override String get cryptoInitFailed => '安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。';
@override String get validationHostRequired => 'ホストアドレスを入力してください';
@override String get validationHostFormat => '形式はIP:ポートである必要があります(例: 192.168.1.100:48632';
@override String get validationSessionIdRequired => 'セッションIDを入力してください';
@override String get validationSessionIdLength => 'セッションIDは8文字である必要があります';
@override String get validationPinRequired => 'PINを入力してください';
@override String get validationPinLength => 'PINは6桁である必要があります';
@override String get connectionTimedOut => '接続がタイムアウトしました。セッションIDとPINを確認してください。';
@override String get sessionNotFound => 'セッションが見つかりませんでした。認証情報を確認してください。';
@override String get connectionTimedOut => '接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください';
@override String get sessionNotFound => 'デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。';
@override String get authFailed => '認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください';
@override String failedToConnect({required Object error}) => '接続に失敗しました: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsJa {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'コンパニオンリモートサーバー',
'settings.companionRemoteServerDescription' => 'ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする',
'search.hint' => '映画、番組、音楽を検索...',
'search.tryDifferentTerm' => '別の検索語をお試しください',
'search.searchYourMedia' => 'メディアを検索',
@@ -1881,10 +1873,10 @@ extension on TranslationsJa {
'libraries.groupings.seasons' => 'シーズン',
'libraries.groupings.episodes' => 'エピソード',
'libraries.groupings.folders' => 'フォルダ',
'about.title' => 'アプリについて',
'about.openSourceLicenses' => 'オープンソースライセンス',
_ => null,
} ?? switch (path) {
'about.title' => 'アプリについて',
'about.openSourceLicenses' => 'オープンソースライセンス',
'about.versionLabel' => ({required Object version}) => 'バージョン ${version}',
'about.appDescription' => 'Flutter製の美しいPlexクライアント',
'about.viewLicensesDescription' => 'サードパーティライブラリのライセンスを表示',
@@ -2076,45 +2068,33 @@ extension on TranslationsJa {
'companionRemote.remoteControl' => 'リモコン',
'companionRemote.controlDesktop' => 'デスクトップデバイスを操作',
'companionRemote.connectedTo' => ({required Object name}) => '${name}に接続中',
'companionRemote.session.creatingSession' => 'リモートセッションを作成中...',
'companionRemote.session.failedToCreate' => 'リモートセッションの作成に失敗しました:',
'companionRemote.session.noSession' => '利用可能なセッションがありません',
'companionRemote.session.scanQrCode' => 'QRコードをスキャン',
'companionRemote.session.orEnterManually' => 'または手動で入力',
'companionRemote.session.startingServer' => 'リモートサーバーを起動中...',
'companionRemote.session.failedToCreate' => 'リモートサーバーの起動に失敗しました:',
'companionRemote.session.hostAddress' => 'ホストアドレス',
'companionRemote.session.sessionId' => 'セッションID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => '接続済み',
'companionRemote.session.waitingForConnection' => '接続を待機中...',
'companionRemote.session.usePhoneToControl' => 'モバイルデバイスでこのアプリを操作',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label}をクリップボードにコピーしました',
'companionRemote.session.copyToClipboard' => 'クリップボードにコピー',
'companionRemote.session.newSession' => '新しいセッション',
'companionRemote.session.serverRunning' => 'リモートサーバー稼働中',
'companionRemote.session.serverStopped' => 'リモートサーバー停止中',
'companionRemote.session.serverRunningDescription' => 'ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます',
'companionRemote.session.serverStoppedDescription' => 'モバイルデバイスの接続を許可するにはサーバーを起動してください',
'companionRemote.session.usePhoneToControl' => 'モバイルデバイスでこのアプリを操作できます',
'companionRemote.session.startServer' => 'サーバーを起動',
'companionRemote.session.stopServer' => 'サーバーを停止',
'companionRemote.session.minimize' => '最小化',
'companionRemote.pairing.scan' => 'スキャン',
'companionRemote.pairing.manual' => '手動',
'companionRemote.pairing.pairWithDesktop' => 'デスクトップとペアリング',
'companionRemote.pairing.enterSessionDetails' => 'デスクトップデバイスに表示されたセッション情報を入力',
'companionRemote.pairing.pairWithDesktop' => 'デスクトップに接続',
'companionRemote.pairing.discoveryDescription' => '同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '8文字のセッションIDを入力',
'companionRemote.pairing.pinHint' => '6桁のPINを入力',
'companionRemote.pairing.connecting' => '接続中...',
'companionRemote.pairing.tips' => 'ヒント',
'companionRemote.pairing.tipDesktop' => 'デスクトップでPlezyを開き、設定またはメニューからコンパニオンリモートを有効にしてください',
'companionRemote.pairing.tipScan' => 'スキャンタブを使用して、デスクトップのQRコードをスキャンして素早くペアリング',
'companionRemote.pairing.tipWifi' => '両方のデバイスが同じWiFiネットワークに接続されていることを確認',
'companionRemote.pairing.cameraPermissionRequired' => 'QRコードをスキャンするにはカメラの権限が必要です。\nデバイスの設定でカメラへのアクセスを許可してください。',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'カメラを起動できませんでした: ${error}',
'companionRemote.pairing.scanInstruction' => 'デスクトップに表示されたQRコードにカメラを向けてください',
'companionRemote.pairing.invalidQrCode' => '無効なQRコード形式',
'companionRemote.pairing.searchingForDevices' => 'デバイスを検索中...',
'companionRemote.pairing.noDevicesFound' => 'ネットワーク上にデバイスが見つかりません',
'companionRemote.pairing.noDevicesHint' => 'デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください',
'companionRemote.pairing.availableDevices' => '利用可能なデバイス',
'companionRemote.pairing.manualConnection' => '手動接続',
'companionRemote.pairing.cryptoInitFailed' => '安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。',
'companionRemote.pairing.validationHostRequired' => 'ホストアドレスを入力してください',
'companionRemote.pairing.validationHostFormat' => '形式はIP:ポートである必要があります(例: 192.168.1.100:48632',
'companionRemote.pairing.validationSessionIdRequired' => 'セッションIDを入力してください',
'companionRemote.pairing.validationSessionIdLength' => 'セッションIDは8文字である必要があります',
'companionRemote.pairing.validationPinRequired' => 'PINを入力してください',
'companionRemote.pairing.validationPinLength' => 'PINは6桁である必要があります',
'companionRemote.pairing.connectionTimedOut' => '接続がタイムアウトしました。セッションIDとPINを確認してください。',
'companionRemote.pairing.sessionNotFound' => 'セッションが見つかりませんでした。認証情報を確認してください。',
'companionRemote.pairing.connectionTimedOut' => '接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください',
'companionRemote.pairing.sessionNotFound' => 'デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。',
'companionRemote.pairing.authFailed' => '認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => '接続に失敗しました: ${error}',
'companionRemote.remote.disconnectConfirm' => 'リモートセッションから切断しますか?',
'companionRemote.remote.reconnecting' => '再接続中...',
+46 -66
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
@override String get companionRemoteServer => '컴패니언 리모트 서버';
@override String get companionRemoteServerDescription => '네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용';
}
// Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionKo implements TranslationsCompanionRemo
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get creatingSession => '원격 세션 생성 중...';
@override String get failedToCreate => '원격 세션 생성 실패:';
@override String get noSession => '사용 가능한 세션이 없습니다';
@override String get scanQrCode => 'QR 코드 스캔';
@override String get orEnterManually => '또는 수동으로 입력';
@override String get startingServer => '원격 서버 시작 중...';
@override String get failedToCreate => '원격 서버를 시작하지 못했습니다:';
@override String get hostAddress => '호스트 주소';
@override String get sessionId => '세션 ID';
@override String get pin => 'PIN';
@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 copiedToClipboard({required Object label}) => '${label}이(가) 클립보드에 복사되었습니다';
@override String get copyToClipboard => '클립보드에 복사';
@override String get newSession => '새 세션';
@override String get startServer => '서버 시작';
@override String get stopServer => '서버 중지';
@override String get minimize => '최소화';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingKo implements TranslationsCompanionRemo
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get scan => '스캔';
@override String get manual => '수동';
@override String get pairWithDesktop => '데스크톱과 페어링';
@override String get enterSessionDetails => '데스크톱 기기에 표시된 세션 정보를 입력하세요';
@override String get pairWithDesktop => '데스크톱에 연결';
@override String get discoveryDescription => '같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다';
@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 tips => '';
@override String get tipDesktop => '데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요';
@override String get tipScan => '스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다';
@override String get tipWifi => '두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요';
@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 searchingForDevices => '기기 검색 중...';
@override String get noDevicesFound => '네트워크에서 기기를 찾을 수 없습니다';
@override String get noDevicesHint => '데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요';
@override String get availableDevices => '사용 가능한 기기';
@override String get manualConnection => '수동 연결';
@override String get cryptoInitFailed => '보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.';
@override String get validationHostRequired => '호스트 주소를 입력하세요';
@override String get validationHostFormat => 'IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)';
@override String get validationSessionIdRequired => '세션 ID를 입력하세요';
@override String get validationSessionIdLength => '세션 ID는 8자리여야 합니다';
@override String get validationPinRequired => 'PIN을 입력하세요';
@override String get validationPinLength => 'PIN은 6자리여야 합니다';
@override String get connectionTimedOut => '연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.';
@override String get sessionNotFound => '세션을 찾을 수 없습니다. 자격 증명을 확인하세요.';
@override String get validationHostFormat => '형식은 IP:포트야 합니다 (예: 192.168.1.100:48632)';
@override String get connectionTimedOut => '연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.';
@override String get sessionNotFound => '기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.';
@override String get authFailed => '인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.';
@override String failedToConnect({required Object error}) => '연결 실패: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsKo {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => '컴패니언 리모트 서버',
'settings.companionRemoteServerDescription' => '네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용',
'search.hint' => '영화, 시리즈, 음악 등을 검색하세요...',
'search.tryDifferentTerm' => '다른 검색어를 시도해 보세요',
'search.searchYourMedia' => '미디어 검색',
@@ -1881,10 +1873,10 @@ extension on TranslationsKo {
'libraries.groupings.seasons' => '시즌',
'libraries.groupings.episodes' => '',
'libraries.groupings.folders' => '폴더',
'about.title' => '소개',
'about.openSourceLicenses' => '오픈소스 라이선스',
_ => null,
} ?? switch (path) {
'about.title' => '소개',
'about.openSourceLicenses' => '오픈소스 라이선스',
'about.versionLabel' => ({required Object version}) => '버전 ${version}',
'about.appDescription' => '아름다운 Flutter Plex 클라이언트',
'about.viewLicensesDescription' => '타사 라이브러리 라이선스 보기',
@@ -2076,45 +2068,33 @@ extension on TranslationsKo {
'companionRemote.remoteControl' => '원격 제어',
'companionRemote.controlDesktop' => '데스크톱 기기 제어',
'companionRemote.connectedTo' => ({required Object name}) => '${name}에 연결됨',
'companionRemote.session.creatingSession' => '원격 세션 생성 중...',
'companionRemote.session.failedToCreate' => '원격 세션 생성 실패:',
'companionRemote.session.noSession' => '사용 가능한 세션이 없습니다',
'companionRemote.session.scanQrCode' => 'QR 코드 스캔',
'companionRemote.session.orEnterManually' => '또는 수동으로 입력',
'companionRemote.session.startingServer' => '원격 서버 시작 중...',
'companionRemote.session.failedToCreate' => '원격 서버를 시작하지 못했습니다:',
'companionRemote.session.hostAddress' => '호스트 주소',
'companionRemote.session.sessionId' => '세션 ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => '연결됨',
'companionRemote.session.waitingForConnection' => '연결 대기 중...',
'companionRemote.session.serverRunning' => '원격 서버 활성',
'companionRemote.session.serverStopped' => '원격 서버 중지됨',
'companionRemote.session.serverRunningDescription' => '네트워크의 모바일 기기가 이 앱을 검색하고 연결할 수 있습니다',
'companionRemote.session.serverStoppedDescription' => '모바일 기기의 연결을 허용하려면 서버를 시작하세요',
'companionRemote.session.usePhoneToControl' => '모바일 기기로 이 앱을 제어하세요',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label}이(가) 클립보드에 복사되었습니다',
'companionRemote.session.copyToClipboard' => '클립보드에 복사',
'companionRemote.session.newSession' => '새 세션',
'companionRemote.session.startServer' => '서버 시작',
'companionRemote.session.stopServer' => '서버 중지',
'companionRemote.session.minimize' => '최소화',
'companionRemote.pairing.scan' => '스캔',
'companionRemote.pairing.manual' => '수동',
'companionRemote.pairing.pairWithDesktop' => '데스크톱과 페어링',
'companionRemote.pairing.enterSessionDetails' => '데스크톱 기기에 표시된 세션 정보를 입력하세요',
'companionRemote.pairing.pairWithDesktop' => '데스크톱에 연결',
'companionRemote.pairing.discoveryDescription' => '같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '8자리 세션 ID 입력',
'companionRemote.pairing.pinHint' => '6자리 PIN 입력',
'companionRemote.pairing.connecting' => '연결 중...',
'companionRemote.pairing.tips' => '',
'companionRemote.pairing.tipDesktop' => '데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요',
'companionRemote.pairing.tipScan' => '스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다',
'companionRemote.pairing.tipWifi' => '두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요',
'companionRemote.pairing.cameraPermissionRequired' => 'QR 코드를 스캔하려면 카메라 권한이 필요합니다.\n기기 설정에서 카메라 접근을 허용해 주세요.',
'companionRemote.pairing.cameraError' => ({required Object error}) => '카메라를 시작할 수 없습니다: ${error}',
'companionRemote.pairing.scanInstruction' => '데스크톱에 표시된 QR 코드에 카메라를 향하세요',
'companionRemote.pairing.invalidQrCode' => '유효하지 않은 QR 코드 형식',
'companionRemote.pairing.searchingForDevices' => '기기 검색 중...',
'companionRemote.pairing.noDevicesFound' => '네트워크에서 기기를 찾을 수 없습니다',
'companionRemote.pairing.noDevicesHint' => '데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요',
'companionRemote.pairing.availableDevices' => '사용 가능한 기기',
'companionRemote.pairing.manualConnection' => '수동 연결',
'companionRemote.pairing.cryptoInitFailed' => '보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.',
'companionRemote.pairing.validationHostRequired' => '호스트 주소를 입력하세요',
'companionRemote.pairing.validationHostFormat' => 'IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => '세션 ID를 입력하세요',
'companionRemote.pairing.validationSessionIdLength' => '세션 ID는 8자리여야 합니다',
'companionRemote.pairing.validationPinRequired' => 'PIN을 입력하세요',
'companionRemote.pairing.validationPinLength' => 'PIN은 6자리여야 합니다',
'companionRemote.pairing.connectionTimedOut' => '연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.',
'companionRemote.pairing.sessionNotFound' => '세션을 찾을 수 없습니다. 자격 증명을 확인하세요.',
'companionRemote.pairing.validationHostFormat' => '형식은 IP:포트야 합니다 (예: 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => '연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.',
'companionRemote.pairing.sessionNotFound' => '기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.',
'companionRemote.pairing.authFailed' => '인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => '연결 실패: ${error}',
'companionRemote.remote.disconnectConfirm' => '원격 세션 연결을 해제하시겠습니까?',
'companionRemote.remote.reconnecting' => '재연결 중...',
+50 -70
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsNb implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionNb implements TranslationsCompanionRemo
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Oppretter fjernøkt...';
@override String get failedToCreate => 'Kunne ikke opprette fjernøkt:';
@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 startingServer => 'Starter fjernserver...';
@override String get failedToCreate => 'Kunne ikke starte fjernserver:';
@override String get hostAddress => 'Vertsadresse';
@override String get sessionId => 'Økt-ID';
@override String get pin => 'PIN';
@override String get connected => 'Tilkoblet';
@override String get waitingForConnection => 'Venter på tilkobling...';
@override String get usePhoneToControl => 'Bruk mobilenheten din til å kontrollere denne appen';
@override String copiedToClipboard({required Object label}) => '${label} kopiert til utklippstavle';
@override String get copyToClipboard => 'Kopier til utklippstavle';
@override String get newSession => 'Ny økt';
@override String get serverRunning => 'Fjernserver aktiv';
@override String get serverStopped => 'Fjernserver stoppet';
@override String get serverRunningDescription => 'Mobilenheter på nettverket ditt kan oppdage og koble til denne appen';
@override String get serverStoppedDescription => 'Start serveren for å la mobilenheter koble til';
@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';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingNb implements TranslationsCompanionRemo
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get scan => 'Skann';
@override String get manual => 'Manuell';
@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 pairWithDesktop => 'Koble til datamaskin';
@override String get discoveryDescription => 'Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk';
@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 tips => 'Tips';
@override String get tipDesktop => 'Åpne Plezy på datamaskinen din og aktiver Følgesvenn-fjernkontroll fra innstillinger eller meny';
@override String get tipScan => 'Bruk Skann-fanen for å raskt koble til ved å skanne QR-koden på datamaskinen din';
@override String get tipWifi => 'Sørg for at begge enhetene er på samme WiFi-nettverk';
@override String get cameraPermissionRequired => 'Kameratillatelse kreves for å skanne QR-koder.\nVennligst gi kameratilgang i enhetsinnstillingene.';
@override String cameraError({required Object error}) => 'Kunne ikke starte kamera: ${error}';
@override String get scanInstruction => 'Pek kameraet mot QR-koden som vises på datamaskinen din';
@override String get invalidQrCode => 'Ugyldig QR-kodeformat';
@override String get validationHostRequired => 'Vennligst skriv inn vertsadresse';
@override String get validationHostFormat => 'Formatet må være IP:port (f.eks. 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Vennligst skriv inn en økt-ID';
@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 get searchingForDevices => 'Søker etter enheter...';
@override String get noDevicesFound => 'Ingen enheter funnet på nettverket ditt';
@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 availableDevices => 'Tilgjengelige enheter';
@override String get manualConnection => 'Manuell tilkobling';
@override String get cryptoInitFailed => 'Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.';
@override String get validationHostRequired => 'Vennligst oppgi vertsadresse';
@override String get validationHostFormat => 'Format må være IP:port (f.eks. 192.168.1.100:48632)';
@override String get connectionTimedOut => 'Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.';
@override String get sessionNotFound => 'Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.';
@override String get authFailed => 'Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.';
@override String failedToConnect({required Object error}) => 'Kunne ikke koble til: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsNb {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Prøv et annet søkeord',
'search.searchYourMedia' => 'Søk i mediene dine',
@@ -1881,10 +1873,10 @@ extension on TranslationsNb {
'libraries.groupings.seasons' => 'Sesonger',
'libraries.groupings.episodes' => 'Episoder',
'libraries.groupings.folders' => 'Mapper',
'about.title' => 'Om',
'about.openSourceLicenses' => 'Åpen kildekode-lisenser',
_ => null,
} ?? switch (path) {
'about.title' => 'Om',
'about.openSourceLicenses' => 'Åpen kildekode-lisenser',
'about.versionLabel' => ({required Object version}) => 'Versjon ${version}',
'about.appDescription' => 'En vakker Plex-klient for Flutter',
'about.viewLicensesDescription' => 'Vis lisenser for tredjepartsbiblioteker',
@@ -2076,45 +2068,33 @@ extension on TranslationsNb {
'companionRemote.remoteControl' => 'Fjernkontroll',
'companionRemote.controlDesktop' => 'Kontroller en stasjonær enhet',
'companionRemote.connectedTo' => ({required Object name}) => 'Tilkoblet ${name}',
'companionRemote.session.creatingSession' => 'Oppretter fjernøkt...',
'companionRemote.session.failedToCreate' => 'Kunne ikke opprette fjernøkt:',
'companionRemote.session.noSession' => 'Ingen økt tilgjengelig',
'companionRemote.session.scanQrCode' => 'Skann QR-kode',
'companionRemote.session.orEnterManually' => 'Eller skriv inn manuelt',
'companionRemote.session.startingServer' => 'Starter fjernserver...',
'companionRemote.session.failedToCreate' => 'Kunne ikke starte fjernserver:',
'companionRemote.session.hostAddress' => 'Vertsadresse',
'companionRemote.session.sessionId' => 'Økt-ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Tilkoblet',
'companionRemote.session.waitingForConnection' => 'Venter på tilkobling...',
'companionRemote.session.usePhoneToControl' => 'Bruk mobilenheten din til å kontrollere denne appen',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} kopiert til utklippstavle',
'companionRemote.session.copyToClipboard' => 'Kopier til utklippstavle',
'companionRemote.session.newSession' => 'Ny økt',
'companionRemote.session.serverRunning' => 'Fjernserver aktiv',
'companionRemote.session.serverStopped' => 'Fjernserver stoppet',
'companionRemote.session.serverRunningDescription' => 'Mobilenheter på nettverket ditt kan oppdage og koble til denne appen',
'companionRemote.session.serverStoppedDescription' => 'Start serveren for å la mobilenheter koble til',
'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.pairing.scan' => 'Skann',
'companionRemote.pairing.manual' => 'Manuell',
'companionRemote.pairing.pairWithDesktop' => 'Koble til stasjonær',
'companionRemote.pairing.enterSessionDetails' => 'Skriv inn øktdetaljene som vises på den stasjonære enheten din',
'companionRemote.pairing.pairWithDesktop' => 'Koble til datamaskin',
'companionRemote.pairing.discoveryDescription' => 'Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk',
'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.tips' => 'Tips',
'companionRemote.pairing.tipDesktop' => 'Åpne Plezy på datamaskinen din og aktiver Følgesvenn-fjernkontroll fra innstillinger eller meny',
'companionRemote.pairing.tipScan' => 'Bruk Skann-fanen for å raskt koble til ved å skanne QR-koden på datamaskinen din',
'companionRemote.pairing.tipWifi' => 'Sørg for at begge enhetene er på samme WiFi-nettverk',
'companionRemote.pairing.cameraPermissionRequired' => 'Kameratillatelse kreves for å skanne QR-koder.\nVennligst gi kameratilgang i enhetsinnstillingene.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kunne ikke starte kamera: ${error}',
'companionRemote.pairing.scanInstruction' => 'Pek kameraet mot QR-koden som vises på datamaskinen din',
'companionRemote.pairing.invalidQrCode' => 'Ugyldig QR-kodeformat',
'companionRemote.pairing.validationHostRequired' => 'Vennligst skriv inn vertsadresse',
'companionRemote.pairing.validationHostFormat' => 'Formatet må være IP:port (f.eks. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Vennligst skriv inn en økt-ID',
'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.searchingForDevices' => 'Søker etter enheter...',
'companionRemote.pairing.noDevicesFound' => 'Ingen enheter funnet på nettverket ditt',
'companionRemote.pairing.noDevicesHint' => 'Sørg for at Plezy er åpen på datamaskinen din og at begge enhetene er på samme WiFi-nettverk',
'companionRemote.pairing.availableDevices' => 'Tilgjengelige enheter',
'companionRemote.pairing.manualConnection' => 'Manuell tilkobling',
'companionRemote.pairing.cryptoInitFailed' => 'Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.',
'companionRemote.pairing.validationHostRequired' => 'Vennligst oppgi vertsadresse',
'companionRemote.pairing.validationHostFormat' => 'Format må være IP:port (f.eks. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.',
'companionRemote.pairing.sessionNotFound' => 'Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.',
'companionRemote.pairing.authFailed' => 'Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke koble til: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vil du koble fra fjernøkten?',
'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 seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionNl implements TranslationsCompanionRemo
final TranslationsNl _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Externe sessie aanmaken...';
@override String get failedToCreate => 'Kan externe sessie niet aanmaken:';
@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 startingServer => 'Externe server starten...';
@override String get failedToCreate => 'Kan externe server niet starten:';
@override String get hostAddress => 'Hostadres';
@override String get sessionId => 'Sessie-ID';
@override String get pin => 'PIN';
@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 copiedToClipboard({required Object label}) => '${label} gekopieerd naar klembord';
@override String get copyToClipboard => 'Kopieer naar klembord';
@override String get newSession => 'Nieuwe sessie';
@override String get startServer => 'Server starten';
@override String get stopServer => 'Server stoppen';
@override String get minimize => 'Minimaliseren';
}
@@ -1298,31 +1297,22 @@ class _TranslationsCompanionRemotePairingNl implements TranslationsCompanionRemo
final TranslationsNl _root; // ignore: unused_field
// Translations
@override String get scan => 'Scannen';
@override String get manual => 'Handmatig';
@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 pairWithDesktop => 'Verbinden met desktop';
@override String get discoveryDescription => 'Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch';
@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 tips => 'Tips';
@override String get tipDesktop => 'Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu';
@override String get tipScan => 'Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen';
@override String get tipWifi => 'Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten';
@override String get cameraPermissionRequired => 'Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.';
@override String cameraError({required Object error}) => 'Kan camera niet starten: ${error}';
@override String get scanInstruction => 'Richt je camera op de QR-code die op je desktop wordt getoond';
@override String get invalidQrCode => 'Ongeldig QR-codeformaat';
@override String get validationHostRequired => 'Voer een hostadres in';
@override String get searchingForDevices => 'Apparaten zoeken...';
@override String get noDevicesFound => 'Geen apparaten gevonden op je netwerk';
@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 availableDevices => 'Beschikbare apparaten';
@override String get manualConnection => 'Handmatige verbinding';
@override String get cryptoInitFailed => 'Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.';
@override String get validationHostRequired => 'Voer het hostadres in';
@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 validationSessionIdLength => 'Sessie-ID moet 8 tekens zijn';
@override String get validationPinRequired => 'Voer een PIN in';
@override String get validationPinLength => 'PIN moet 6 cijfers zijn';
@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}';
@override String get connectionTimedOut => 'Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.';
@override String get sessionNotFound => 'Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.';
@override String get authFailed => 'Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.';
@override String failedToConnect({required Object error}) => 'Kan niet verbinden: ${error}';
}
// Path: companionRemote.remote
@@ -1600,6 +1590,8 @@ extension on TranslationsNl {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Probeer een andere zoekterm',
'search.searchYourMedia' => 'Zoek in je media',
@@ -1881,10 +1873,10 @@ extension on TranslationsNl {
'libraries.groupings.seasons' => 'Seizoenen',
'libraries.groupings.episodes' => 'Afleveringen',
'libraries.groupings.folders' => 'Mappen',
'about.title' => 'Over',
'about.openSourceLicenses' => 'Open Source licenties',
_ => null,
} ?? switch (path) {
'about.title' => 'Over',
'about.openSourceLicenses' => 'Open Source licenties',
'about.versionLabel' => ({required Object version}) => 'Versie ${version}',
'about.appDescription' => 'Een mooie Plex client voor Flutter',
'about.viewLicensesDescription' => 'Bekijk licenties van third-party bibliotheken',
@@ -2076,46 +2068,34 @@ extension on TranslationsNl {
'companionRemote.remoteControl' => 'Afstandsbediening',
'companionRemote.controlDesktop' => 'Bedien een desktop-apparaat',
'companionRemote.connectedTo' => ({required Object name}) => 'Verbonden met ${name}',
'companionRemote.session.creatingSession' => 'Externe sessie aanmaken...',
'companionRemote.session.failedToCreate' => 'Kan externe sessie niet aanmaken:',
'companionRemote.session.noSession' => 'Geen sessie beschikbaar',
'companionRemote.session.scanQrCode' => 'Scan QR-code',
'companionRemote.session.orEnterManually' => 'Of voer handmatig in',
'companionRemote.session.startingServer' => 'Externe server starten...',
'companionRemote.session.failedToCreate' => 'Kan externe server niet starten:',
'companionRemote.session.hostAddress' => 'Hostadres',
'companionRemote.session.sessionId' => 'Sessie-ID',
'companionRemote.session.pin' => 'PIN',
'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.copiedToClipboard' => ({required Object label}) => '${label} gekopieerd naar klembord',
'companionRemote.session.copyToClipboard' => 'Kopieer naar klembord',
'companionRemote.session.newSession' => 'Nieuwe sessie',
'companionRemote.session.startServer' => 'Server starten',
'companionRemote.session.stopServer' => 'Server stoppen',
'companionRemote.session.minimize' => 'Minimaliseren',
'companionRemote.pairing.scan' => 'Scannen',
'companionRemote.pairing.manual' => 'Handmatig',
'companionRemote.pairing.pairWithDesktop' => 'Koppelen met desktop',
'companionRemote.pairing.enterSessionDetails' => 'Voer de sessiegegevens in die op je desktop-apparaat worden getoond',
'companionRemote.pairing.pairWithDesktop' => 'Verbinden met desktop',
'companionRemote.pairing.discoveryDescription' => 'Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch',
'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.tips' => 'Tips',
'companionRemote.pairing.tipDesktop' => 'Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu',
'companionRemote.pairing.tipScan' => 'Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen',
'companionRemote.pairing.tipWifi' => 'Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten',
'companionRemote.pairing.cameraPermissionRequired' => 'Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kan camera niet starten: ${error}',
'companionRemote.pairing.scanInstruction' => 'Richt je camera op de QR-code die op je desktop wordt getoond',
'companionRemote.pairing.invalidQrCode' => 'Ongeldig QR-codeformaat',
'companionRemote.pairing.validationHostRequired' => 'Voer een hostadres in',
'companionRemote.pairing.searchingForDevices' => 'Apparaten zoeken...',
'companionRemote.pairing.noDevicesFound' => 'Geen apparaten gevonden op je netwerk',
'companionRemote.pairing.noDevicesHint' => 'Zorg ervoor dat Plezy geopend is op je desktop en dat beide apparaten op hetzelfde WiFi-netwerk zitten',
'companionRemote.pairing.availableDevices' => 'Beschikbare apparaten',
'companionRemote.pairing.manualConnection' => 'Handmatige verbinding',
'companionRemote.pairing.cryptoInitFailed' => 'Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.',
'companionRemote.pairing.validationHostRequired' => 'Voer het hostadres in',
'companionRemote.pairing.validationHostFormat' => 'Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Voer een sessie-ID in',
'companionRemote.pairing.validationSessionIdLength' => 'Sessie-ID moet 8 tekens zijn',
'companionRemote.pairing.validationPinRequired' => 'Voer een PIN in',
'companionRemote.pairing.validationPinLength' => 'PIN moet 6 cijfers zijn',
'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.pairing.connectionTimedOut' => 'Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.',
'companionRemote.pairing.sessionNotFound' => 'Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.',
'companionRemote.pairing.authFailed' => 'Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kan niet verbinden: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Wil je de verbinding met de externe sessie verbreken?',
'companionRemote.remote.reconnecting' => 'Opnieuw verbinden...',
'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 seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionPl implements TranslationsCompanionRemo
final TranslationsPl _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Tworzenie sesji zdalnej...';
@override String get failedToCreate => 'Nie udało się utworzyć sesji zdalnej:';
@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 startingServer => 'Uruchamianie serwera zdalnego...';
@override String get failedToCreate => 'Nie udało się uruchomić serwera zdalnego:';
@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 waitingForConnection => 'Oczekiwanie na połączenie...';
@override String get usePhoneToControl => 'Użyj urządzenia mobilnego do sterowania tą aplikacją';
@override String copiedToClipboard({required Object label}) => '${label} skopiowane do schowka';
@override String get copyToClipboard => 'Kopiuj do schowka';
@override String get newSession => 'Nowa sesja';
@override String get serverRunning => 'Serwer zdalny aktywny';
@override String get serverStopped => 'Serwer zdalny zatrzymany';
@override String get serverRunningDescription => 'Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć';
@override String get serverStoppedDescription => 'Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych';
@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';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingPl implements TranslationsCompanionRemo
final TranslationsPl _root; // ignore: unused_field
// Translations
@override String get scan => 'Skanuj';
@override String get manual => 'cznie';
@override String get pairWithDesktop => 'Sparuj z komputerem';
@override String get enterSessionDetails => 'Wprowadź dane sesji wyświetlone na urządzeniu desktop';
@override String get pairWithDesktop => 'Połącz z komputerem';
@override String get discoveryDescription => 'Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie';
@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 tips => 'Wskazówki';
@override String get tipDesktop => 'Otwórz Plezy na komputerze i włącz Pilota zdalnego w ustawieniach lub menu';
@override String get tipScan => 'Użyj zakładki Skanuj, aby szybko sparować skanując kod QR na komputerze';
@override String get tipWifi => 'Upewnij się, że oba urządzenia są w tej samej sieci WiFi';
@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 cameraError({required Object error}) => 'Nie udało się uruchomić kamery: ${error}';
@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 searchingForDevices => 'Szukanie urządzeń...';
@override String get noDevicesFound => 'Nie znaleziono urządzeń w sieci';
@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 availableDevices => 'Dostępne urządzenia';
@override String get manualConnection => 'Połączenie ręczne';
@override String get cryptoInitFailed => 'Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.';
@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 validationSessionIdRequired => 'Wprowadź ID sesji';
@override String get validationSessionIdLength => 'ID sesji musi mieć 8 znaków';
@override String get validationPinRequired => 'Wprowadź PIN';
@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 get connectionTimedOut => 'Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.';
@override String get sessionNotFound => 'Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.';
@override String get authFailed => 'Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.';
@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.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Spróbuj innego wyszukiwania',
'search.searchYourMedia' => 'Przeszukaj swoje media',
@@ -1881,10 +1873,10 @@ extension on TranslationsPl {
'libraries.groupings.seasons' => 'Sezony',
'libraries.groupings.episodes' => 'Odcinki',
'libraries.groupings.folders' => 'Foldery',
'about.title' => 'O aplikacji',
'about.openSourceLicenses' => 'Licencje open source',
_ => null,
} ?? switch (path) {
'about.title' => 'O aplikacji',
'about.openSourceLicenses' => 'Licencje open source',
'about.versionLabel' => ({required Object version}) => 'Wersja ${version}',
'about.appDescription' => 'Piękny klient Plex na Flutter',
'about.viewLicensesDescription' => 'Zobacz licencje bibliotek zewnętrznych',
@@ -2076,45 +2068,33 @@ extension on TranslationsPl {
'companionRemote.remoteControl' => 'Pilot zdalny',
'companionRemote.controlDesktop' => 'Steruj urządzeniem desktop',
'companionRemote.connectedTo' => ({required Object name}) => 'Połączono z ${name}',
'companionRemote.session.creatingSession' => 'Tworzenie sesji zdalnej...',
'companionRemote.session.failedToCreate' => 'Nie udało się utworzyć sesji zdalnej:',
'companionRemote.session.noSession' => 'Brak dostępnej sesji',
'companionRemote.session.scanQrCode' => 'Zeskanuj kod QR',
'companionRemote.session.orEnterManually' => 'Lub wprowadź ręcznie',
'companionRemote.session.startingServer' => 'Uruchamianie serwera zdalnego...',
'companionRemote.session.failedToCreate' => 'Nie udało się uruchomić serwera zdalnego:',
'companionRemote.session.hostAddress' => 'Adres hosta',
'companionRemote.session.sessionId' => 'ID sesji',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Połączono',
'companionRemote.session.waitingForConnection' => 'Oczekiwanie na połączenie...',
'companionRemote.session.usePhoneToControl' => 'Użyj urządzenia mobilnego do sterowania tą aplikacją',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} skopiowane do schowka',
'companionRemote.session.copyToClipboard' => 'Kopiuj do schowka',
'companionRemote.session.newSession' => 'Nowa sesja',
'companionRemote.session.serverRunning' => 'Serwer zdalny aktywny',
'companionRemote.session.serverStopped' => 'Serwer zdalny zatrzymany',
'companionRemote.session.serverRunningDescription' => 'Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć',
'companionRemote.session.serverStoppedDescription' => 'Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych',
'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.pairing.scan' => 'Skanuj',
'companionRemote.pairing.manual' => 'cznie',
'companionRemote.pairing.pairWithDesktop' => 'Sparuj z komputerem',
'companionRemote.pairing.enterSessionDetails' => 'Wprowadź dane sesji wyświetlone na urządzeniu desktop',
'companionRemote.pairing.pairWithDesktop' => 'Połącz z komputerem',
'companionRemote.pairing.discoveryDescription' => 'Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie',
'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.tips' => 'Wskazówki',
'companionRemote.pairing.tipDesktop' => 'Otwórz Plezy na komputerze i włącz Pilota zdalnego w ustawieniach lub menu',
'companionRemote.pairing.tipScan' => 'Użyj zakładki Skanuj, aby szybko sparować skanując kod QR na komputerze',
'companionRemote.pairing.tipWifi' => 'Upewnij się, że oba urządzenia są w tej samej sieci WiFi',
'companionRemote.pairing.cameraPermissionRequired' => 'Uprawnienie do kamery jest wymagane do skanowania kodów QR.\nPrzyznaj dostęp do kamery w ustawieniach urządzenia.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Nie udało się uruchomić kamery: ${error}',
'companionRemote.pairing.scanInstruction' => 'Skieruj kamerę na kod QR wyświetlony na komputerze',
'companionRemote.pairing.invalidQrCode' => 'Nieprawidłowy format kodu QR',
'companionRemote.pairing.searchingForDevices' => 'Szukanie urządzeń...',
'companionRemote.pairing.noDevicesFound' => 'Nie znaleziono urządzeń w sieci',
'companionRemote.pairing.noDevicesHint' => 'Upewnij się, że Plezy jest otwarte na komputerze i oba urządzenia są w tej samej sieci WiFi',
'companionRemote.pairing.availableDevices' => 'Dostępne urządzenia',
'companionRemote.pairing.manualConnection' => 'Połączenie ręczne',
'companionRemote.pairing.cryptoInitFailed' => 'Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.',
'companionRemote.pairing.validationHostRequired' => 'Wprowadź adres hosta',
'companionRemote.pairing.validationHostFormat' => 'Format musi być IP:port (np. 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Wprowadź ID sesji',
'companionRemote.pairing.validationSessionIdLength' => 'ID sesji musi mieć 8 znaków',
'companionRemote.pairing.validationPinRequired' => 'Wprowadź PIN',
'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.connectionTimedOut' => 'Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.',
'companionRemote.pairing.sessionNotFound' => 'Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.',
'companionRemote.pairing.authFailed' => 'Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.',
'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.reconnecting' => 'Ponowne łączenie...',
+54 -74
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsPt implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionPt implements TranslationsCompanionRemo
final TranslationsPt _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Criando sessão remota...';
@override String get failedToCreate => 'Falha ao criar sessão remota:';
@override String get noSession => 'Nenhuma sessão disponível';
@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 startingServer => 'A iniciar servidor remoto...';
@override String get failedToCreate => 'Falha ao iniciar o servidor remoto:';
@override String get hostAddress => 'Endereço do host';
@override String get connected => 'Conectado';
@override String get waitingForConnection => 'Aguardando conexão...';
@override String get usePhoneToControl => 'Use seu dispositivo móvel para controlar este app';
@override String copiedToClipboard({required Object label}) => '${label} copiado para a área de transferência';
@override String get copyToClipboard => 'Copiar para área de transferência';
@override String get newSession => 'Nova Sessão';
@override String get serverRunning => 'Servidor remoto ativo';
@override String get serverStopped => 'Servidor remoto parado';
@override String get serverRunningDescription => 'Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação';
@override String get serverStoppedDescription => 'Inicie o servidor para permitir que dispositivos móveis se conectem';
@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';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingPt implements TranslationsCompanionRemo
final TranslationsPt _root; // ignore: unused_field
// Translations
@override String get scan => 'Escanear';
@override String get manual => 'Manual';
@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 pairWithDesktop => 'Conectar ao desktop';
@override String get discoveryDescription => 'Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente';
@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 pinHint => 'Insira o PIN de 6 dígitos';
@override String get connecting => 'Conectando...';
@override String get tips => 'Dicas';
@override String get tipDesktop => 'Abra o Plezy no desktop e ative o Controle Remoto nas configurações ou menu';
@override String get tipScan => 'Use a aba Escanear para parear rapidamente escaneando o QR code no seu desktop';
@override String get tipWifi => 'Certifique-se de que ambos os dispositivos estão na mesma rede WiFi';
@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 cameraError({required Object error}) => 'Não foi possível iniciar a câmera: ${error}';
@override String get scanInstruction => 'Aponte sua câmera para o QR code exibido no seu desktop';
@override String get invalidQrCode => 'Formato de QR code inválido';
@override String get validationHostRequired => 'Insira o endereço do host';
@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 get connecting => 'A conectar...';
@override String get searchingForDevices => 'A procurar dispositivos...';
@override String get noDevicesFound => 'Nenhum dispositivo encontrado na sua rede';
@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 availableDevices => 'Dispositivos disponíveis';
@override String get manualConnection => 'Conexão manual';
@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 validationHostRequired => 'Introduza o endereço do host';
@override String get validationHostFormat => 'O formato deve ser IP:porta (ex. 192.168.1.100:48632)';
@override String get connectionTimedOut => 'Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.';
@override String get sessionNotFound => 'Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.';
@override String get authFailed => 'Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.';
@override String failedToConnect({required Object error}) => 'Falha ao conectar: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsPt {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Tente um termo de busca diferente',
'search.searchYourMedia' => 'Buscar suas mídias',
@@ -1881,10 +1873,10 @@ extension on TranslationsPt {
'libraries.groupings.seasons' => 'Temporadas',
'libraries.groupings.episodes' => 'Episódios',
'libraries.groupings.folders' => 'Pastas',
'about.title' => 'Sobre',
'about.openSourceLicenses' => 'Licenças Open Source',
_ => null,
} ?? switch (path) {
'about.title' => 'Sobre',
'about.openSourceLicenses' => 'Licenças Open Source',
'about.versionLabel' => ({required Object version}) => 'Versão ${version}',
'about.appDescription' => 'Um belo cliente Plex para Flutter',
'about.viewLicensesDescription' => 'Ver licenças de bibliotecas de terceiros',
@@ -2076,45 +2068,33 @@ extension on TranslationsPt {
'companionRemote.remoteControl' => 'Controle Remoto',
'companionRemote.controlDesktop' => 'Controlar um dispositivo desktop',
'companionRemote.connectedTo' => ({required Object name}) => 'Conectado a ${name}',
'companionRemote.session.creatingSession' => 'Criando sessão remota...',
'companionRemote.session.failedToCreate' => 'Falha ao criar sessão remota:',
'companionRemote.session.noSession' => 'Nenhuma sessão disponível',
'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.startingServer' => 'A iniciar servidor remoto...',
'companionRemote.session.failedToCreate' => 'Falha ao iniciar o servidor remoto:',
'companionRemote.session.hostAddress' => 'Endereço do host',
'companionRemote.session.connected' => 'Conectado',
'companionRemote.session.waitingForConnection' => 'Aguardando conexão...',
'companionRemote.session.usePhoneToControl' => 'Use seu dispositivo móvel para controlar este app',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} copiado para a área de transferência',
'companionRemote.session.copyToClipboard' => 'Copiar para área de transferência',
'companionRemote.session.newSession' => 'Nova Sessão',
'companionRemote.session.serverRunning' => 'Servidor remoto ativo',
'companionRemote.session.serverStopped' => 'Servidor remoto parado',
'companionRemote.session.serverRunningDescription' => 'Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação',
'companionRemote.session.serverStoppedDescription' => 'Inicie o servidor para permitir que dispositivos móveis se conectem',
'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.pairing.scan' => 'Escanear',
'companionRemote.pairing.manual' => 'Manual',
'companionRemote.pairing.pairWithDesktop' => 'Parear com Desktop',
'companionRemote.pairing.enterSessionDetails' => 'Insira os detalhes da sessão exibidos no seu dispositivo desktop',
'companionRemote.pairing.pairWithDesktop' => 'Conectar ao desktop',
'companionRemote.pairing.discoveryDescription' => 'Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Insira o ID da sessão de 8 caracteres',
'companionRemote.pairing.pinHint' => 'Insira o PIN de 6 dígitos',
'companionRemote.pairing.connecting' => 'Conectando...',
'companionRemote.pairing.tips' => 'Dicas',
'companionRemote.pairing.tipDesktop' => 'Abra o Plezy no desktop e ative o Controle Remoto nas configurações ou menu',
'companionRemote.pairing.tipScan' => 'Use a aba Escanear para parear rapidamente escaneando o QR code no seu desktop',
'companionRemote.pairing.tipWifi' => 'Certifique-se de que ambos os dispositivos estão na mesma rede WiFi',
'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.cameraError' => ({required Object error}) => 'Não foi possível iniciar a câmera: ${error}',
'companionRemote.pairing.scanInstruction' => 'Aponte sua câmera para o QR code exibido no seu desktop',
'companionRemote.pairing.invalidQrCode' => 'Formato de QR code inválido',
'companionRemote.pairing.validationHostRequired' => 'Insira o endereço do host',
'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.connecting' => 'A conectar...',
'companionRemote.pairing.searchingForDevices' => 'A procurar dispositivos...',
'companionRemote.pairing.noDevicesFound' => 'Nenhum dispositivo encontrado na sua rede',
'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.availableDevices' => 'Dispositivos disponíveis',
'companionRemote.pairing.manualConnection' => 'Conexão manual',
'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.validationHostRequired' => 'Introduza o endereço do host',
'companionRemote.pairing.validationHostFormat' => 'O formato deve ser IP:porta (ex. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.',
'companionRemote.pairing.sessionNotFound' => 'Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.',
'companionRemote.pairing.authFailed' => 'Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Falha ao conectar: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Deseja desconectar da sessão remota?',
'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 seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
@override String get companionRemoteServer => 'Сервер удалённого управления';
@override String get companionRemoteServerDescription => 'Разрешить мобильным устройствам в сети управлять этим приложением';
}
// Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionRu implements TranslationsCompanionRemo
final TranslationsRu _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Создание удалённой сессии...';
@override String get failedToCreate => 'Не удалось создать удалённую сессию:';
@override String get noSession => 'Нет доступных сессий';
@override String get scanQrCode => 'Сканировать QR-код';
@override String get orEnterManually => 'Или введите вручную';
@override String get startingServer => 'Запуск удалённого сервера...';
@override String get failedToCreate => 'Не удалось запустить удалённый сервер:';
@override String get hostAddress => 'Адрес хоста';
@override String get sessionId => 'ID сессии';
@override String get pin => 'PIN';
@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 copiedToClipboard({required Object label}) => '${label} скопировано в буфер обмена';
@override String get copyToClipboard => 'Скопировать в буфер обмена';
@override String get newSession => 'Новая сессия';
@override String get startServer => 'Запустить сервер';
@override String get stopServer => 'Остановить сервер';
@override String get minimize => 'Свернуть';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingRu implements TranslationsCompanionRemo
final TranslationsRu _root; // ignore: unused_field
// Translations
@override String get scan => 'Сканировать';
@override String get manual => 'Вручную';
@override String get pairWithDesktop => 'Сопряжение с ПК';
@override String get enterSessionDetails => 'Введите данные сессии, показанные на десктопном устройстве';
@override String get pairWithDesktop => 'Подключиться к компьютеру';
@override String get discoveryDescription => 'Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически';
@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 tips => 'Советы';
@override String get tipDesktop => 'Откройте Plezy на десктопе и включите пульт управления в настройках или меню';
@override String get tipScan => 'Используйте вкладку «Сканировать» для быстрого сопряжения через QR-код на десктопе';
@override String get tipWifi => 'Убедитесь, что оба устройства находятся в одной WiFi-сети';
@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 searchingForDevices => 'Поиск устройств...';
@override String get noDevicesFound => 'Устройства в вашей сети не найдены';
@override String get noDevicesHint => 'Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi';
@override String get availableDevices => 'Доступные устройства';
@override String get manualConnection => 'Ручное подключение';
@override String get cryptoInitFailed => 'Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.';
@override String get validationHostRequired => 'Введите адрес хоста';
@override String get validationHostFormat => 'Формат должен быть IP:порт (например, 192.168.1.100:48632)';
@override String get validationSessionIdRequired => 'Введите ID сессии';
@override String get validationSessionIdLength => 'ID сессии должен содержать 8 символов';
@override String get validationPinRequired => 'Введите PIN';
@override String get validationPinLength => 'PIN должен содержать 6 цифр';
@override String get connectionTimedOut => 'Время подключения истекло. Проверьте ID сессии и PIN.';
@override String get sessionNotFound => 'Сессия не найдена. Проверьте ваши учётные данные.';
@override String get connectionTimedOut => 'Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.';
@override String get sessionNotFound => 'Устройство не найдено. Убедитесь, что Plezy запущен на хосте.';
@override String get authFailed => 'Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.';
@override String failedToConnect({required Object error}) => 'Не удалось подключиться: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsRu {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => 'Сервер удалённого управления',
'settings.companionRemoteServerDescription' => 'Разрешить мобильным устройствам в сети управлять этим приложением',
'search.hint' => 'Поиск фильмов, сериалов, музыки...',
'search.tryDifferentTerm' => 'Попробуйте другой запрос',
'search.searchYourMedia' => 'Поиск в вашей медиатеке',
@@ -1881,10 +1873,10 @@ extension on TranslationsRu {
'libraries.groupings.seasons' => 'Сезоны',
'libraries.groupings.episodes' => 'Эпизоды',
'libraries.groupings.folders' => 'Папки',
'about.title' => 'О приложении',
'about.openSourceLicenses' => 'Лицензии открытого ПО',
_ => null,
} ?? switch (path) {
'about.title' => 'О приложении',
'about.openSourceLicenses' => 'Лицензии открытого ПО',
'about.versionLabel' => ({required Object version}) => 'Версия ${version}',
'about.appDescription' => 'Красивый клиент Plex на Flutter',
'about.viewLicensesDescription' => 'Просмотр лицензий сторонних библиотек',
@@ -2076,45 +2068,33 @@ extension on TranslationsRu {
'companionRemote.remoteControl' => 'Пульт управления',
'companionRemote.controlDesktop' => 'Управлять десктопным устройством',
'companionRemote.connectedTo' => ({required Object name}) => 'Подключено к ${name}',
'companionRemote.session.creatingSession' => 'Создание удалённой сессии...',
'companionRemote.session.failedToCreate' => 'Не удалось создать удалённую сессию:',
'companionRemote.session.noSession' => 'Нет доступных сессий',
'companionRemote.session.scanQrCode' => 'Сканировать QR-код',
'companionRemote.session.orEnterManually' => 'Или введите вручную',
'companionRemote.session.startingServer' => 'Запуск удалённого сервера...',
'companionRemote.session.failedToCreate' => 'Не удалось запустить удалённый сервер:',
'companionRemote.session.hostAddress' => 'Адрес хоста',
'companionRemote.session.sessionId' => 'ID сессии',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => 'Подключено',
'companionRemote.session.waitingForConnection' => 'Ожидание подключения...',
'companionRemote.session.serverRunning' => 'Удалённый сервер активен',
'companionRemote.session.serverStopped' => 'Удалённый сервер остановлен',
'companionRemote.session.serverRunningDescription' => 'Мобильные устройства в вашей сети могут обнаруживать это приложение и подключаться к нему',
'companionRemote.session.serverStoppedDescription' => 'Запустите сервер, чтобы разрешить подключение мобильных устройств',
'companionRemote.session.usePhoneToControl' => 'Используйте мобильное устройство для управления этим приложением',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label} скопировано в буфер обмена',
'companionRemote.session.copyToClipboard' => 'Скопировать в буфер обмена',
'companionRemote.session.newSession' => 'Новая сессия',
'companionRemote.session.startServer' => 'Запустить сервер',
'companionRemote.session.stopServer' => 'Остановить сервер',
'companionRemote.session.minimize' => 'Свернуть',
'companionRemote.pairing.scan' => 'Сканировать',
'companionRemote.pairing.manual' => 'Вручную',
'companionRemote.pairing.pairWithDesktop' => 'Сопряжение с ПК',
'companionRemote.pairing.enterSessionDetails' => 'Введите данные сессии, показанные на десктопном устройстве',
'companionRemote.pairing.pairWithDesktop' => 'Подключиться к компьютеру',
'companionRemote.pairing.discoveryDescription' => 'Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => 'Введите 8-символьный ID сессии',
'companionRemote.pairing.pinHint' => 'Введите 6-значный PIN',
'companionRemote.pairing.connecting' => 'Подключение...',
'companionRemote.pairing.tips' => 'Советы',
'companionRemote.pairing.tipDesktop' => 'Откройте Plezy на десктопе и включите пульт управления в настройках или меню',
'companionRemote.pairing.tipScan' => 'Используйте вкладку «Сканировать» для быстрого сопряжения через QR-код на десктопе',
'companionRemote.pairing.tipWifi' => 'Убедитесь, что оба устройства находятся в одной WiFi-сети',
'companionRemote.pairing.cameraPermissionRequired' => 'Для сканирования QR-кодов требуется разрешение на камеру.\nПредоставьте доступ к камере в настройках устройства.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Не удалось запустить камеру: ${error}',
'companionRemote.pairing.scanInstruction' => 'Наведите камеру на QR-код, показанный на десктопе',
'companionRemote.pairing.invalidQrCode' => 'Недопустимый формат QR-кода',
'companionRemote.pairing.searchingForDevices' => 'Поиск устройств...',
'companionRemote.pairing.noDevicesFound' => 'Устройства в вашей сети не найдены',
'companionRemote.pairing.noDevicesHint' => 'Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi',
'companionRemote.pairing.availableDevices' => 'Доступные устройства',
'companionRemote.pairing.manualConnection' => 'Ручное подключение',
'companionRemote.pairing.cryptoInitFailed' => 'Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.',
'companionRemote.pairing.validationHostRequired' => 'Введите адрес хоста',
'companionRemote.pairing.validationHostFormat' => 'Формат должен быть IP:порт (например, 192.168.1.100:48632)',
'companionRemote.pairing.validationSessionIdRequired' => 'Введите ID сессии',
'companionRemote.pairing.validationSessionIdLength' => 'ID сессии должен содержать 8 символов',
'companionRemote.pairing.validationPinRequired' => 'Введите PIN',
'companionRemote.pairing.validationPinLength' => 'PIN должен содержать 6 цифр',
'companionRemote.pairing.connectionTimedOut' => 'Время подключения истекло. Проверьте ID сессии и PIN.',
'companionRemote.pairing.sessionNotFound' => 'Сессия не найдена. Проверьте ваши учётные данные.',
'companionRemote.pairing.connectionTimedOut' => 'Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.',
'companionRemote.pairing.sessionNotFound' => 'Устройство не найдено. Убедитесь, что Plezy запущен на хосте.',
'companionRemote.pairing.authFailed' => 'Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Не удалось подключиться: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Отключиться от удалённой сессии?',
'companionRemote.remote.reconnecting' => 'Переподключение...',
+46 -66
View File
@@ -358,6 +358,8 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn {
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@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
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionSv implements TranslationsCompanionRemo
final TranslationsSv _root; // ignore: unused_field
// Translations
@override String get creatingSession => 'Skapar fjärrsession...';
@override String get failedToCreate => 'Kunde inte skapa fjärrsession:';
@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 startingServer => 'Startar fjärrserver...';
@override String get failedToCreate => 'Kunde inte starta fjärrserver:';
@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 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 copiedToClipboard({required Object label}) => '${label} kopierad till urklipp';
@override String get copyToClipboard => 'Kopiera till urklipp';
@override String get newSession => 'Ny session';
@override String get startServer => 'Starta server';
@override String get stopServer => 'Stoppa server';
@override String get minimize => 'Minimera';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingSv implements TranslationsCompanionRemo
final TranslationsSv _root; // ignore: unused_field
// Translations
@override String get scan => 'Skanna';
@override String get manual => 'Manuell';
@override String get pairWithDesktop => 'Parkoppla med dator';
@override String get enterSessionDetails => 'Ange sessionsuppgifterna som visas på din datorenhet';
@override String get pairWithDesktop => 'Anslut till dator';
@override String get discoveryDescription => 'Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt';
@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 tips => 'Tips';
@override String get tipDesktop => 'Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny';
@override String get tipScan => 'Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator';
@override String get tipWifi => 'Se till att båda enheterna är på samma WiFi-nätverk';
@override String get cameraPermissionRequired => 'Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.';
@override String cameraError({required Object error}) => 'Kunde inte starta kameran: ${error}';
@override String get scanInstruction => 'Rikta kameran mot QR-koden som visas på din dator';
@override String get invalidQrCode => 'Ogiltigt QR-kodformat';
@override String get validationHostRequired => 'Ange en värdadress';
@override String get searchingForDevices => 'Söker efter enheter...';
@override String get noDevicesFound => 'Inga enheter hittades i ditt nätverk';
@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 availableDevices => 'Tillgängliga enheter';
@override String get manualConnection => 'Manuell anslutning';
@override String get cryptoInitFailed => 'Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.';
@override String get validationHostRequired => 'Ange värdadress';
@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 validationSessionIdLength => 'Sessions-ID måste vara 8 tecken';
@override String get validationPinRequired => 'Ange en PIN';
@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 get connectionTimedOut => 'Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.';
@override String get sessionNotFound => 'Kunde inte hitta enheten. Se till att Plezy körs på värden.';
@override String get authFailed => 'Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.';
@override String failedToConnect({required Object error}) => 'Kunde inte ansluta: ${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsSv {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'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.tryDifferentTerm' => 'Prova en annan sökterm',
'search.searchYourMedia' => 'Sök i dina media',
@@ -1881,10 +1873,10 @@ extension on TranslationsSv {
'libraries.groupings.seasons' => 'Säsonger',
'libraries.groupings.episodes' => 'Avsnitt',
'libraries.groupings.folders' => 'Mappar',
'about.title' => 'Om',
'about.openSourceLicenses' => 'Öppen källkod-licenser',
_ => null,
} ?? switch (path) {
'about.title' => 'Om',
'about.openSourceLicenses' => 'Öppen källkod-licenser',
'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'En vacker Plex-klient för Flutter',
'about.viewLicensesDescription' => 'Visa licenser för tredjepartsbibliotek',
@@ -2076,45 +2068,33 @@ extension on TranslationsSv {
'companionRemote.remoteControl' => 'Fjärrkontroll',
'companionRemote.controlDesktop' => 'Styr en datorenhet',
'companionRemote.connectedTo' => ({required Object name}) => 'Ansluten till ${name}',
'companionRemote.session.creatingSession' => 'Skapar fjärrsession...',
'companionRemote.session.failedToCreate' => 'Kunde inte skapa fjärrsession:',
'companionRemote.session.noSession' => 'Ingen session tillgänglig',
'companionRemote.session.scanQrCode' => 'Skanna QR-kod',
'companionRemote.session.orEnterManually' => 'Eller ange manuellt',
'companionRemote.session.startingServer' => 'Startar fjärrserver...',
'companionRemote.session.failedToCreate' => 'Kunde inte starta fjärrserver:',
'companionRemote.session.hostAddress' => 'Värdadress',
'companionRemote.session.sessionId' => 'Sessions-ID',
'companionRemote.session.pin' => 'PIN',
'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.copiedToClipboard' => ({required Object label}) => '${label} kopierad till urklipp',
'companionRemote.session.copyToClipboard' => 'Kopiera till urklipp',
'companionRemote.session.newSession' => 'Ny session',
'companionRemote.session.startServer' => 'Starta server',
'companionRemote.session.stopServer' => 'Stoppa server',
'companionRemote.session.minimize' => 'Minimera',
'companionRemote.pairing.scan' => 'Skanna',
'companionRemote.pairing.manual' => 'Manuell',
'companionRemote.pairing.pairWithDesktop' => 'Parkoppla med dator',
'companionRemote.pairing.enterSessionDetails' => 'Ange sessionsuppgifterna som visas på din datorenhet',
'companionRemote.pairing.pairWithDesktop' => 'Anslut till dator',
'companionRemote.pairing.discoveryDescription' => 'Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt',
'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.tips' => 'Tips',
'companionRemote.pairing.tipDesktop' => 'Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny',
'companionRemote.pairing.tipScan' => 'Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator',
'companionRemote.pairing.tipWifi' => 'Se till att båda enheterna är på samma WiFi-nätverk',
'companionRemote.pairing.cameraPermissionRequired' => 'Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.',
'companionRemote.pairing.cameraError' => ({required Object error}) => 'Kunde inte starta kameran: ${error}',
'companionRemote.pairing.scanInstruction' => 'Rikta kameran mot QR-koden som visas på din dator',
'companionRemote.pairing.invalidQrCode' => 'Ogiltigt QR-kodformat',
'companionRemote.pairing.validationHostRequired' => 'Ange en värdadress',
'companionRemote.pairing.searchingForDevices' => 'Söker efter enheter...',
'companionRemote.pairing.noDevicesFound' => 'Inga enheter hittades i ditt nätverk',
'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.availableDevices' => 'Tillgängliga enheter',
'companionRemote.pairing.manualConnection' => 'Manuell anslutning',
'companionRemote.pairing.cryptoInitFailed' => 'Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.',
'companionRemote.pairing.validationHostRequired' => 'Ange värdadress',
'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.validationSessionIdLength' => 'Sessions-ID måste vara 8 tecken',
'companionRemote.pairing.validationPinRequired' => 'Ange en PIN',
'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.connectionTimedOut' => 'Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.',
'companionRemote.pairing.sessionNotFound' => 'Kunde inte hitta enheten. Se till att Plezy körs på värden.',
'companionRemote.pairing.authFailed' => 'Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunde inte ansluta: ${error}',
'companionRemote.remote.disconnectConfirm' => 'Vill du koppla från fjärrsessionen?',
'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 seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
@override String get companionRemoteServer => '配套遥控服务器';
@override String get companionRemoteServerDescription => '允许网络上的移动设备控制此应用';
}
// Path: search
@@ -1274,20 +1276,17 @@ class _TranslationsCompanionRemoteSessionZh implements TranslationsCompanionRemo
final TranslationsZh _root; // ignore: unused_field
// Translations
@override String get creatingSession => '正在创建远程会话...';
@override String get failedToCreate => '创建远程会话失败:';
@override String get noSession => '没有可用的会话';
@override String get scanQrCode => '扫描 QR 码';
@override String get orEnterManually => '或手动输入';
@override String get startingServer => '正在启动远程服务器...';
@override String get failedToCreate => '启动远程服务器失败:';
@override String get hostAddress => '主机地址';
@override String get sessionId => '会话 ID';
@override String get pin => 'PIN';
@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 copiedToClipboard({required Object label}) => '${label}已复制到剪贴板';
@override String get copyToClipboard => '复制到剪贴板';
@override String get newSession => '新建会话';
@override String get startServer => '启动服务器';
@override String get stopServer => '停止服务器';
@override String get minimize => '最小化';
}
@@ -1298,30 +1297,21 @@ class _TranslationsCompanionRemotePairingZh implements TranslationsCompanionRemo
final TranslationsZh _root; // ignore: unused_field
// Translations
@override String get scan => '扫描';
@override String get manual => '手动';
@override String get pairWithDesktop => '与桌面配对';
@override String get enterSessionDetails => '输入桌面设备上显示的会话信息';
@override String get pairWithDesktop => '连接到桌面';
@override String get discoveryDescription => '网络上使用相同Plex账户运行Plezy的设备将自动显示';
@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 tips => '提示';
@override String get tipDesktop => '在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote';
@override String get tipScan => '使用扫描选项卡扫描桌面上的 QR 码以快速配对';
@override String get tipWifi => '请确保两台设备连接到同一个 WiFi 网络';
@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 connecting => '正在连接...';
@override String get searchingForDevices => '正在搜索设备...';
@override String get noDevicesFound => '未在网络上找到设备';
@override String get noDevicesHint => '请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上';
@override String get availableDevices => '可用设备';
@override String get manualConnection => '手动连接';
@override String get cryptoInitFailed => '无法初始化安全连接。请确保已登录Plex账户。';
@override String get validationHostRequired => '请输入主机地址';
@override String get validationHostFormat => '格式必须为 IP:端口(例如 192.168.1.100:48632';
@override String get validationSessionIdRequired => '请输入会话 ID';
@override String get validationSessionIdLength => '会话 ID 必须为8个字符';
@override String get validationPinRequired => '请输入 PIN';
@override String get validationPinLength => 'PIN 必须为6位数字';
@override String get connectionTimedOut => '连接超时。请检查会话 ID 和 PIN。';
@override String get sessionNotFound => '找不到会话。请检查您的凭据。';
@override String get validationHostFormat => '格式必须为IP:端口(例如 192.168.1.100:48632';
@override String get connectionTimedOut => '连接超时。请确保两台设备在同一网络上。';
@override String get sessionNotFound => '未找到设备。请确保Plezy正在主机上运行。';
@override String get authFailed => '认证失败。请确保两台设备使用相同的Plex账户。';
@override String failedToConnect({required Object error}) => '连接失败:${error}';
}
@@ -1600,6 +1590,8 @@ extension on TranslationsZh {
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'settings.companionRemoteServer' => '配套遥控服务器',
'settings.companionRemoteServerDescription' => '允许网络上的移动设备控制此应用',
'search.hint' => '搜索电影、系列、音乐...',
'search.tryDifferentTerm' => '尝试不同的搜索词',
'search.searchYourMedia' => '搜索媒体',
@@ -1881,10 +1873,10 @@ extension on TranslationsZh {
'libraries.groupings.seasons' => '',
'libraries.groupings.episodes' => '',
'libraries.groupings.folders' => '文件夹',
'about.title' => '关于',
'about.openSourceLicenses' => '开源许可证',
_ => null,
} ?? switch (path) {
'about.title' => '关于',
'about.openSourceLicenses' => '开源许可证',
'about.versionLabel' => ({required Object version}) => '版本 ${version}',
'about.appDescription' => '一款精美的 Flutter Plex 客户端',
'about.viewLicensesDescription' => '查看第三方库的许可证',
@@ -2076,45 +2068,33 @@ extension on TranslationsZh {
'companionRemote.remoteControl' => '远程控制',
'companionRemote.controlDesktop' => '控制桌面设备',
'companionRemote.connectedTo' => ({required Object name}) => '已连接到 ${name}',
'companionRemote.session.creatingSession' => '正在创建远程会话...',
'companionRemote.session.failedToCreate' => '创建远程会话失败:',
'companionRemote.session.noSession' => '没有可用的会话',
'companionRemote.session.scanQrCode' => '扫描 QR 码',
'companionRemote.session.orEnterManually' => '或手动输入',
'companionRemote.session.startingServer' => '正在启动远程服务器...',
'companionRemote.session.failedToCreate' => '启动远程服务器失败:',
'companionRemote.session.hostAddress' => '主机地址',
'companionRemote.session.sessionId' => '会话 ID',
'companionRemote.session.pin' => 'PIN',
'companionRemote.session.connected' => '已连接',
'companionRemote.session.waitingForConnection' => '等待连接中...',
'companionRemote.session.serverRunning' => '远程服务器已启动',
'companionRemote.session.serverStopped' => '远程服务器已停止',
'companionRemote.session.serverRunningDescription' => '网络上的移动设备可以发现并连接到此应用',
'companionRemote.session.serverStoppedDescription' => '启动服务器以允许移动设备连接',
'companionRemote.session.usePhoneToControl' => '使用移动设备控制此应用',
'companionRemote.session.copiedToClipboard' => ({required Object label}) => '${label}已复制到剪贴板',
'companionRemote.session.copyToClipboard' => '复制到剪贴板',
'companionRemote.session.newSession' => '新建会话',
'companionRemote.session.startServer' => '启动服务器',
'companionRemote.session.stopServer' => '停止服务器',
'companionRemote.session.minimize' => '最小化',
'companionRemote.pairing.scan' => '扫描',
'companionRemote.pairing.manual' => '手动',
'companionRemote.pairing.pairWithDesktop' => '与桌面配对',
'companionRemote.pairing.enterSessionDetails' => '输入桌面设备上显示的会话信息',
'companionRemote.pairing.pairWithDesktop' => '连接到桌面',
'companionRemote.pairing.discoveryDescription' => '网络上使用相同Plex账户运行Plezy的设备将自动显示',
'companionRemote.pairing.hostAddressHint' => '192.168.1.100:48632',
'companionRemote.pairing.sessionIdHint' => '输入8位会话 ID',
'companionRemote.pairing.pinHint' => '输入6位 PIN',
'companionRemote.pairing.connecting' => '连接中...',
'companionRemote.pairing.tips' => '提示',
'companionRemote.pairing.tipDesktop' => '在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote',
'companionRemote.pairing.tipScan' => '使用扫描选项卡扫描桌面上的 QR 码以快速配对',
'companionRemote.pairing.tipWifi' => '请确保两台设备连接到同一个 WiFi 网络',
'companionRemote.pairing.cameraPermissionRequired' => '扫描 QR 码需要相机权限。\n请在设备设置中授予相机访问权限。',
'companionRemote.pairing.cameraError' => ({required Object error}) => '无法启动相机:${error}',
'companionRemote.pairing.scanInstruction' => '将相机对准桌面上显示的 QR 码',
'companionRemote.pairing.invalidQrCode' => '无效的 QR 码格式',
'companionRemote.pairing.connecting' => '正在连接...',
'companionRemote.pairing.searchingForDevices' => '正在搜索设备...',
'companionRemote.pairing.noDevicesFound' => '未在网络上找到设备',
'companionRemote.pairing.noDevicesHint' => '请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上',
'companionRemote.pairing.availableDevices' => '可用设备',
'companionRemote.pairing.manualConnection' => '手动连接',
'companionRemote.pairing.cryptoInitFailed' => '无法初始化安全连接。请确保已登录Plex账户。',
'companionRemote.pairing.validationHostRequired' => '请输入主机地址',
'companionRemote.pairing.validationHostFormat' => '格式必须为 IP:端口(例如 192.168.1.100:48632',
'companionRemote.pairing.validationSessionIdRequired' => '请输入会话 ID',
'companionRemote.pairing.validationSessionIdLength' => '会话 ID 必须为8个字符',
'companionRemote.pairing.validationPinRequired' => '请输入 PIN',
'companionRemote.pairing.validationPinLength' => 'PIN 必须为6位数字',
'companionRemote.pairing.connectionTimedOut' => '连接超时。请检查会话 ID 和 PIN。',
'companionRemote.pairing.sessionNotFound' => '找不到会话。请检查您的凭据。',
'companionRemote.pairing.validationHostFormat' => '格式必须为IP:端口(例如 192.168.1.100:48632',
'companionRemote.pairing.connectionTimedOut' => '连接超时。请确保两台设备在同一网络上。',
'companionRemote.pairing.sessionNotFound' => '未找到设备。请确保Plezy正在主机上运行。',
'companionRemote.pairing.authFailed' => '认证失败。请确保两台设备使用相同的Plex账户。',
'companionRemote.pairing.failedToConnect' => ({required Object error}) => '连接失败:${error}',
'companionRemote.remote.disconnectConfirm' => '是否要断开远程会话的连接?',
'companionRemote.remote.reconnecting' => '重新连接中...',
+23 -33
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"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": {
"hint": "Sök filmer, serier, musik...",
@@ -778,47 +780,35 @@
"controlDesktop": "Styr en datorenhet",
"connectedTo": "Ansluten till ${name}",
"session": {
"creatingSession": "Skapar fjärrsession...",
"failedToCreate": "Kunde inte skapa fjärrsession:",
"noSession": "Ingen session tillgänglig",
"scanQrCode": "Skanna QR-kod",
"orEnterManually": "Eller ange manuellt",
"startingServer": "Startar fjärrserver...",
"failedToCreate": "Kunde inte starta fjärrserver:",
"hostAddress": "Värdadress",
"sessionId": "Sessions-ID",
"pin": "PIN",
"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",
"copiedToClipboard": "${label} kopierad till urklipp",
"copyToClipboard": "Kopiera till urklipp",
"newSession": "Ny session",
"startServer": "Starta server",
"stopServer": "Stoppa server",
"minimize": "Minimera"
},
"pairing": {
"scan": "Skanna",
"manual": "Manuell",
"pairWithDesktop": "Parkoppla med dator",
"enterSessionDetails": "Ange sessionsuppgifterna som visas på din datorenhet",
"pairWithDesktop": "Anslut till dator",
"discoveryDescription": "Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Ange 8-teckens sessions-ID",
"pinHint": "Ange 6-siffrig PIN",
"connecting": "Ansluter...",
"tips": "Tips",
"tipDesktop": "Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny",
"tipScan": "Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator",
"tipWifi": "Se till att båda enheterna är på samma WiFi-nätverk",
"cameraPermissionRequired": "Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.",
"cameraError": "Kunde inte starta kameran: ${error}",
"scanInstruction": "Rikta kameran mot QR-koden som visas på din dator",
"invalidQrCode": "Ogiltigt QR-kodformat",
"validationHostRequired": "Ange en värdadress",
"searchingForDevices": "Söker efter enheter...",
"noDevicesFound": "Inga enheter hittades i ditt nätverk",
"noDevicesHint": "Se till att Plezy är öppet på din dator och att båda enheterna är på samma WiFi-nätverk",
"availableDevices": "Tillgängliga enheter",
"manualConnection": "Manuell anslutning",
"cryptoInitFailed": "Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.",
"validationHostRequired": "Ange värdadress",
"validationHostFormat": "Format måste vara IP:port (t.ex. 192.168.1.100:48632)",
"validationSessionIdRequired": "Ange ett sessions-ID",
"validationSessionIdLength": "Sessions-ID måste vara 8 tecken",
"validationPinRequired": "Ange en PIN",
"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.",
"connectionTimedOut": "Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.",
"sessionNotFound": "Kunde inte hitta enheten. Se till att Plezy körs på värden.",
"authFailed": "Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.",
"failedToConnect": "Kunde inte ansluta: ${error}"
},
"remote": {
+24 -34
View File
@@ -238,7 +238,9 @@
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
"behavior": "Behavior",
"companionRemoteServer": "配套遥控服务器",
"companionRemoteServerDescription": "允许网络上的移动设备控制此应用"
},
"search": {
"hint": "搜索电影、系列、音乐...",
@@ -778,47 +780,35 @@
"controlDesktop": "控制桌面设备",
"connectedTo": "已连接到 ${name}",
"session": {
"creatingSession": "正在创建远程会话...",
"failedToCreate": "创建远程会话失败:",
"noSession": "没有可用的会话",
"scanQrCode": "扫描 QR 码",
"orEnterManually": "或手动输入",
"startingServer": "正在启动远程服务器...",
"failedToCreate": "启动远程服务器失败:",
"hostAddress": "主机地址",
"sessionId": "会话 ID",
"pin": "PIN",
"connected": "已连接",
"waitingForConnection": "等待连接中...",
"serverRunning": "远程服务器已启动",
"serverStopped": "远程服务器已停止",
"serverRunningDescription": "网络上的移动设备可以发现并连接到此应用",
"serverStoppedDescription": "启动服务器以允许移动设备连接",
"usePhoneToControl": "使用移动设备控制此应用",
"copiedToClipboard": "${label}已复制到剪贴板",
"copyToClipboard": "复制到剪贴板",
"newSession": "新建会话",
"startServer": "启动服务器",
"stopServer": "停止服务器",
"minimize": "最小化"
},
"pairing": {
"scan": "扫描",
"manual": "手动",
"pairWithDesktop": "与桌面配对",
"enterSessionDetails": "输入桌面设备上显示的会话信息",
"pairWithDesktop": "连接到桌面",
"discoveryDescription": "网络上使用相同Plex账户运行Plezy的设备将自动显示",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "输入8位会话 ID",
"pinHint": "输入6位 PIN",
"connecting": "连接中...",
"tips": "提示",
"tipDesktop": "在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote",
"tipScan": "使用扫描选项卡扫描桌面上的 QR 码以快速配对",
"tipWifi": "请确保两台设备连接到同一个 WiFi 网络",
"cameraPermissionRequired": "扫描 QR 码需要相机权限。\n请在设备设置中授予相机访问权限。",
"cameraError": "无法启动相机:${error}",
"scanInstruction": "将相机对准桌面上显示的 QR 码",
"invalidQrCode": "无效的 QR 码格式",
"connecting": "正在连接...",
"searchingForDevices": "正在搜索设备...",
"noDevicesFound": "未在网络上找到设备",
"noDevicesHint": "请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上",
"availableDevices": "可用设备",
"manualConnection": "手动连接",
"cryptoInitFailed": "无法初始化安全连接。请确保已登录Plex账户。",
"validationHostRequired": "请输入主机地址",
"validationHostFormat": "格式必须为 IP:端口(例如 192.168.1.100:48632",
"validationSessionIdRequired": "请输入会话 ID",
"validationSessionIdLength": "会话 ID 必须为8个字符",
"validationPinRequired": "请输入 PIN",
"validationPinLength": "PIN 必须为6位数字",
"connectionTimedOut": "连接超时。请检查会话 ID 和 PIN。",
"sessionNotFound": "找不到会话。请检查您的凭据。",
"validationHostFormat": "格式必须为IP:端口(例如 192.168.1.100:48632",
"connectionTimedOut": "连接超时。请确保两台设备在同一网络上。",
"sessionNotFound": "未找到设备。请确保Plezy正在主机上运行。",
"authFailed": "认证失败。请确保两台设备使用相同的Plex账户。",
"failedToConnect": "连接失败:${error}"
},
"remote": {
@@ -56,8 +56,6 @@ class RemoteDevice {
@JsonSerializable()
class RemoteSession {
final String sessionId;
final String pin;
@JsonKey(unknownEnumValue: RemoteSessionRole.remote)
final RemoteSessionRole role;
@JsonKey(unknownEnumValue: RemoteSessionStatus.disconnected)
@@ -67,8 +65,6 @@ class RemoteSession {
final String? errorMessage;
RemoteSession({
required this.sessionId,
required this.pin,
required this.role,
this.status = RemoteSessionStatus.disconnected,
this.connectedDevice,
@@ -85,8 +81,6 @@ class RemoteSession {
Map<String, dynamic> toJson() => _$RemoteSessionToJson(this);
RemoteSession copyWith({
String? sessionId,
String? pin,
RemoteSessionRole? role,
RemoteSessionStatus? status,
RemoteDevice? connectedDevice,
@@ -96,8 +90,6 @@ class RemoteSession {
bool clearErrorMessage = false,
}) {
return RemoteSession(
sessionId: sessionId ?? this.sessionId,
pin: pin ?? this.pin,
role: role ?? this.role,
status: status ?? this.status,
connectedDevice: clearConnectedDevice ? null : (connectedDevice ?? this.connectedDevice),
@@ -29,8 +29,6 @@ Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) =>
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
RemoteSession(
sessionId: json['sessionId'] as String,
pin: json['pin'] as String,
role: $enumDecode(
_$RemoteSessionRoleEnumMap,
json['role'],
@@ -56,8 +54,6 @@ RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) =>
<String, dynamic>{
'sessionId': instance.sessionId,
'pin': instance.pin,
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
'connectedDevice': instance.connectedDevice,
+254 -102
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_session.dart';
import '../models/plex_home.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';
export '../services/companion_remote/lan_discovery_service.dart' show DiscoveredHost;
typedef CommandReceivedCallback = void Function(RemoteCommand command);
class CompanionRemoteProvider with ChangeNotifier {
RemoteSession? _session;
CompanionRemotePeerService? _peerService;
LanDiscoveryService? _discoveryService;
String _deviceName = 'Unknown Device';
String _platform = 'unknown';
bool _isPlayerActive = false;
@@ -23,9 +30,17 @@ class CompanionRemoteProvider with ChangeNotifier {
Timer? _reconnectTimer;
int _reconnectAttempts = 0;
bool _intentionalDisconnect = false;
String? _lastSessionId;
String? _lastPin;
String? _lastHostAddress;
// Reconnection context (only hostAddresses and hostClientId are connection-specific)
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;
@@ -43,10 +58,9 @@ class CompanionRemoteProvider with ChangeNotifier {
bool get isConnected => _session?.isConnected ?? false;
RemoteSession? get session => _session;
RemoteSessionStatus get status => _session?.status ?? RemoteSessionStatus.disconnected;
String? get sessionId => _session?.sessionId;
String? get pin => _session?.pin;
RemoteDevice? get connectedDevice => _session?.connectedDevice;
bool get isPlayerActive => _isPlayerActive;
bool get isHostServerRunning => _peerService?.isServerRunning ?? false;
CompanionRemoteProvider() {
_initializeDeviceInfo();
@@ -86,6 +100,219 @@ class CompanionRemoteProvider with ChangeNotifier {
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() {
_commandSubscription = _peerService!.onCommandReceived.listen(
(command) {
@@ -118,7 +345,6 @@ class CompanionRemoteProvider with ChangeNotifier {
_session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true);
notifyListeners();
} else if (isHost) {
// Host keeps the server running — the client will reconnect on its own
_session = _session?.copyWith(
status: RemoteSessionStatus.reconnecting,
clearConnectedDevice: true,
@@ -183,93 +409,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_statusSubscription = null;
}
Future<({String sessionId, String pin, List<String> addresses})> createSession() async {
await leaveSession();
appLogger.d('CompanionRemote: Creating session as host');
_peerService = CompanionRemotePeerService();
_setupPeerServiceListeners();
try {
final result = await _peerService!.createSession(_deviceName, _platform);
_session = RemoteSession(
sessionId: result.sessionId,
pin: result.pin,
role: RemoteSessionRole.host,
status: RemoteSessionStatus.connected,
);
notifyListeners();
appLogger.d(
'CompanionRemote: Session created - ID: ${result.sessionId}, PIN: ${result.pin}, 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;
}
}
// ── Commands ──
void sendCommand(RemoteCommandType type, {Map<String, dynamic>? data}) {
if (_peerService == null || !isConnected) {
@@ -281,6 +421,8 @@ class CompanionRemoteProvider with ChangeNotifier {
_peerService!.sendCommand(RemoteCommand(type: type, data: data));
}
// ── Reconnection ──
void _scheduleReconnect() {
if (_reconnectAttempts >= _maxReconnectAttempts) {
appLogger.w('CompanionRemote: Max reconnect attempts reached');
@@ -293,7 +435,7 @@ class CompanionRemoteProvider with ChangeNotifier {
return;
}
final delay = Duration(seconds: 1 << _reconnectAttempts); // 1s, 2s, 4s, 8s, 16s
final delay = Duration(seconds: 1 << _reconnectAttempts);
_reconnectAttempts++;
appLogger.d('CompanionRemote: Reconnect attempt $_reconnectAttempts in ${delay.inSeconds}s');
@@ -302,8 +444,8 @@ class CompanionRemoteProvider with ChangeNotifier {
}
Future<void> _attemptReconnect() async {
if (_lastSessionId == null || _lastPin == null || _lastHostAddress == null) {
appLogger.w('CompanionRemote: No stored credentials for reconnect');
if (_lastHostAddresses == null || !isCryptoReady) {
appLogger.w('CompanionRemote: No stored context for reconnect');
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost');
notifyListeners();
return;
@@ -311,7 +453,6 @@ class CompanionRemoteProvider with ChangeNotifier {
try {
appLogger.d('CompanionRemote: Attempting reconnect...');
// Clean up old peer service without triggering intentional disconnect
_cleanupSubscriptions();
try {
await _peerService?.disconnect();
@@ -320,7 +461,15 @@ class CompanionRemoteProvider with ChangeNotifier {
_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);
_reconnectAttempts = 0;
@@ -334,14 +483,12 @@ class CompanionRemoteProvider with ChangeNotifier {
}
}
/// Immediately retry reconnection, skipping the backoff wait
void retryReconnectNow() {
_reconnectTimer?.cancel();
_reconnectAttempts = 0;
_attemptReconnect();
}
/// Cancel ongoing reconnection attempts
void cancelReconnect() {
_reconnectTimer?.cancel();
_reconnectAttempts = 0;
@@ -354,7 +501,8 @@ class CompanionRemoteProvider with ChangeNotifier {
_reconnectTimer?.cancel();
_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');
await _peerService!.disconnect();
_peerService = null;
@@ -362,7 +510,9 @@ class CompanionRemoteProvider with ChangeNotifier {
_cleanupSubscriptions();
if (!isHost) {
_session = null;
}
_isPlayerActive = false;
_intentionalDisconnect = false;
notifyListeners();
@@ -371,7 +521,9 @@ class CompanionRemoteProvider with ChangeNotifier {
@override
void dispose() {
_reconnectTimer?.cancel();
leaveSession();
_discoveryService?.dispose();
_peerService?.dispose();
RemoteAuthService.instance.clearCache();
super.dispose();
}
}
+228 -317
View File
@@ -1,12 +1,11 @@
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:provider/provider.dart';
import '../../i18n/strings.g.dart';
import '../../providers/companion_remote_provider.dart';
import '../../providers/user_profile_provider.dart';
import '../../utils/app_logger.dart';
class PairingScreen extends StatefulWidget {
@@ -18,58 +17,97 @@ class PairingScreen extends StatefulWidget {
class _PairingScreenState extends State<PairingScreen> {
final _hostAddressController = TextEditingController();
final _sessionIdController = TextEditingController();
final _pinController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isConnecting = false;
String? _errorMessage;
int _selectedTab = 0;
bool _showManualEntry = false;
bool _cryptoReady = false;
// QR scanner state
MobileScannerController? _scannerController;
String? _lastScannedCode;
bool _hasCamera = false;
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;
StreamSubscription<List<DiscoveredHost>>? _discoverySubscription;
List<DiscoveredHost> _hosts = [];
bool _isSearching = true;
Timer? _searchTimeout;
@override
void initState() {
super.initState();
_checkCamera();
_initCryptoAndDiscover();
}
Future<void> _checkCamera() async {
if (Platform.isIOS) {
setState(() => _hasCamera = true);
return;
Future<void> _initCryptoAndDiscover() async {
final provider = context.read<CompanionRemoteProvider>();
final home = context.read<UserProfileProvider>().home;
await provider.ensureCryptoReady(home);
if (!mounted) return;
if (provider.isCryptoReady) {
setState(() => _cryptoReady = true);
_startDiscovery();
} else {
setState(() {
_cryptoReady = false;
_isSearching = false;
_errorMessage = t.companionRemote.pairing.cryptoInitFailed;
});
}
if (!Platform.isAndroid) return;
try {
const channel = MethodChannel('com.plezy/theme');
final result = await channel.invokeMethod<bool>('hasCamera');
if (mounted) setState(() => _hasCamera = result ?? false);
} catch (e) {
debugPrint('Camera check failed: $e');
}
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
void dispose() {
_hostAddressController.dispose();
_sessionIdController.dispose();
_pinController.dispose();
_scannerController?.dispose();
_discoverySubscription?.cancel();
_searchTimeout?.cancel();
context.read<CompanionRemoteProvider>().stopDiscovery();
super.dispose();
}
Future<void> _connect() async {
if (!_formKey.currentState!.validate()) {
return;
Future<void> _connectToHost(DiscoveredHost host) async {
setState(() {
_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(() {
_isConnecting = true;
@@ -78,17 +116,13 @@ class _PairingScreenState extends State<PairingScreen> {
try {
final provider = context.read<CompanionRemoteProvider>();
await provider.joinSession(
_sessionIdController.text.trim().toUpperCase(),
_pinController.text.trim(),
_hostAddressController.text.trim(),
);
await provider.connectToManualHost(_hostAddressController.text.trim());
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
appLogger.e('Failed to join remote session', error: e);
appLogger.e('Failed to connect to manual host', error: e);
if (!mounted) return;
setState(() {
_isConnecting = false;
@@ -102,73 +136,22 @@ class _PairingScreenState extends State<PairingScreen> {
return t.companionRemote.pairing.connectionTimedOut;
} else if (error.contains('Failed to connect')) {
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: ', ''));
}
void _handleQrCode(String data) {
// Debounce: don't process the same code twice
if (data == _lastScannedCode) return;
_lastScannedCode = data;
// Strip URL wrapper if present (e.g. "https://plezy.app/scan#ip1,ip2|port|sid|pin")
final payload = data.contains('#') ? data.split('#').last : data;
final parts = payload.split('|');
if (parts.length == 4) {
final ipsField = parts.first;
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!;
});
IconData _platformIcon(String platform) {
switch (platform.toLowerCase()) {
case 'macos':
return Icons.desktop_mac;
case 'windows':
return Icons.desktop_windows;
case 'linux':
return Icons.computer;
default:
return Icons.devices;
}
}
@@ -178,133 +161,18 @@ class _PairingScreenState extends State<PairingScreen> {
appBar: AppBar(
title: Text(t.companionRemote.connectToDevice),
),
body: Column(
children: [
if (_canScan)
SegmentedButton<int>(
segments: [
ButtonSegment(
value: _scanTabIndex,
label: Text(t.companionRemote.pairing.scan),
icon: const Icon(Icons.qr_code_scanner),
),
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()),
],
),
body: _buildBody(),
);
}
Widget _buildTabContent() {
if (_selectedTab == _scanTabIndex && _canScan) return _buildScanTab();
return _buildManualEntryTab();
}
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(
children: [
Text(
t.companionRemote.pairing.scanInstruction,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
if (_errorMessage != null) ...[
const SizedBox(height: 12),
Card(
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.onErrorContainer),
const SizedBox(width: 12),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer),
),
),
],
),
),
),
],
],
),
),
],
);
}
Widget _buildManualEntryTab() {
Widget _buildBody() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(Icons.keyboard, size: 64, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 24),
Icon(Icons.devices, size: 64, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 16),
Text(
t.companionRemote.pairing.pairWithDesktop,
style: Theme.of(context).textTheme.headlineMedium,
@@ -312,11 +180,12 @@ class _PairingScreenState extends State<PairingScreen> {
),
const SizedBox(height: 8),
Text(
t.companionRemote.pairing.enterSessionDetails,
t.companionRemote.pairing.discoveryDescription,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
const SizedBox(height: 24),
if (_errorMessage != null) ...[
Card(
color: Theme.of(context).colorScheme.errorContainer,
@@ -338,6 +207,139 @@ class _PairingScreenState extends State<PairingScreen> {
),
const SizedBox(height: 16),
],
// Discovered hosts
_buildDiscoverySection(),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
// Manual entry fallback
_buildManualEntrySection(),
],
),
);
}
Widget _buildDiscoverySection() {
if (!_cryptoReady) {
return Card(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Icon(Icons.warning_amber, size: 48, color: Colors.orange),
const SizedBox(height: 12),
Text(
t.companionRemote.pairing.cryptoInitFailed,
textAlign: TextAlign.center,
),
],
),
),
);
}
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 _buildManualEntrySection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => setState(() => _showManualEntry = !_showManualEntry),
child: Row(
children: [
Icon(
_showManualEntry ? Icons.expand_less : Icons.expand_more,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
t.companionRemote.pairing.manualConnection,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
if (_showManualEntry) ...[
const SizedBox(height: 16),
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _hostAddressController,
decoration: InputDecoration(
@@ -345,17 +347,11 @@ class _PairingScreenState extends State<PairingScreen> {
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;
@@ -365,103 +361,18 @@ class _PairingScreenState extends State<PairingScreen> {
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,
onPressed: _isConnecting ? null : _connectManual,
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;
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
void dispose() {
WidgetsBinding.instance.removeObserver(this);
@@ -45,6 +45,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
String _creditsPattern = settings.SettingsService.defaultCreditsPattern;
int _maxVolume = 100;
bool _enableDiscordRPC = false;
bool _enableCompanionRemoteServer = false;
bool _autoPip = true;
bool _matchContentFrameRate = false;
bool _matchRefreshRate = false;
@@ -84,6 +85,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
_creditsPattern = _settingsService.getCreditsPattern();
_maxVolume = _settingsService.getMaxVolume();
_enableDiscordRPC = _settingsService.getEnableDiscordRPC();
_enableCompanionRemoteServer = _settingsService.getEnableCompanionRemoteServer();
_autoPip = _settingsService.getAutoPip();
_matchContentFrameRate = _settingsService.getMatchContentFrameRate();
_matchRefreshRate = _settingsService.getMatchRefreshRate();
@@ -142,6 +144,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
// --- Behavior ---
SettingsSectionHeader(t.settings.behavior),
if (DiscordRPCService.isAvailable) _buildDiscordRPC(),
if (PlatformDetector.shouldActAsRemoteHost(context)) _buildCompanionRemoteServer(),
_buildRememberTrackSelections(),
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() {
return SwitchListTile(
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 '../../utils/app_logger.dart';
import '../base_peer_service.dart';
import 'remote_auth_service.dart';
// Re-export so callers that import from here get the types.
export '../base_peer_service.dart' show PeerError, PeerErrorType;
@@ -27,12 +28,16 @@ class CompanionRemotePeerService with KeepaliveMixin {
// Client-side (remote) fields
IOWebSocketChannel? _channel;
String? _sessionId;
String? _pin;
String? _myPeerId;
String? _hostAddress; // Format: "ip:port"
RemoteSessionRole? _role;
// Encrypted channel state
List<int>? _sessionEncKey;
int _sendCounter = 0;
int _recvCounter = 0;
bool _isAuthenticated = false;
final _commandReceivedController = StreamController<RemoteCommand>.broadcast();
final _deviceConnectedController = StreamController<RemoteDevice>.broadcast();
final _deviceDisconnectedController = StreamController<void>.broadcast();
@@ -45,9 +50,9 @@ class CompanionRemotePeerService with KeepaliveMixin {
@override
Duration get pongTimeout => Duration.zero; // No pong timeout; host just replies inline
// Auth rate limiting
int _failedAuthAttempts = 0;
DateTime? _authLockoutUntil;
// Auth rate limiting (per source IP)
final Map<String, int> _failedAuthAttempts = {};
final Map<String, DateTime> _authLockouts = {};
static const int _maxFailedAuthAttempts = 5;
static const Duration _authLockoutDuration = Duration(seconds: 30);
@@ -57,25 +62,12 @@ class CompanionRemotePeerService with KeepaliveMixin {
Stream<RemotePeerError> get onError => _errorController.stream;
Stream<RemoteSessionStatus> get onConnectionStateChanged => _connectionStateController.stream;
String? get sessionId => _sessionId;
String? get pin => _pin;
String? get myPeerId => _myPeerId;
String? get hostAddress => _hostAddress;
RemoteSessionRole? get role => _role;
bool get isHost => _role == RemoteSessionRole.host;
bool get isConnected => _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 {
try {
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 platform,
List<int> homeSecret,
String clientIdentifier,
List<String> homeUserUUIDs,
) async {
if (_server != null) {
await disconnect();
}
_role = RemoteSessionRole.host;
_sessionId = _generateSessionId();
_pin = _generatePin();
_myPeerId = 'host-$_sessionId';
_myPeerId = 'host';
try {
// Try preferred port first, fallback to OS-assigned port
const int preferredPort = 48632;
try {
@@ -141,12 +134,20 @@ class CompanionRemotePeerService with KeepaliveMixin {
appLogger.d('CompanionRemote: Host server started, addresses: $addresses');
// Listen for WebSocket connections
_server!.listen((HttpRequest request) async {
if (request.uri.path == '/ws') {
try {
final socket = await WebSocketTransformer.upgrade(request);
_handleNewWebSocketConnection(socket, deviceName, platform);
final sourceIp = request.connectionInfo?.remoteAddress.address ?? 'unknown';
_handleNewWebSocketConnection(
socket,
deviceName,
platform,
homeSecret,
clientIdentifier,
homeUserUUIDs,
sourceIp,
);
} catch (e) {
appLogger.e('CompanionRemote: Failed to upgrade WebSocket', error: e);
}
@@ -158,7 +159,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
_connectionStateController.add(RemoteSessionStatus.connected);
return (sessionId: _sessionId!, pin: _pin!, addresses: addresses);
return (addresses: addresses, port: port);
} catch (e) {
appLogger.e('CompanionRemote: Failed to create server', error: e);
_errorController.add(
@@ -172,11 +173,36 @@ class CompanionRemotePeerService with KeepaliveMixin {
}
}
void _handleNewWebSocketConnection(WebSocket socket, String hostDeviceName, String hostPlatform) {
appLogger.d('CompanionRemote: New WebSocket connection');
void _handleNewWebSocketConnection(
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;
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
authTimeout = Timer(const Duration(seconds: 10), () {
@@ -187,31 +213,69 @@ class CompanionRemotePeerService with KeepaliveMixin {
});
socket.listen(
(data) {
(data) async {
try {
if (!isAuthenticated) {
final json = jsonDecode(data as String) as Map<String, dynamic>;
if (!isAuthenticated) {
// First message must be authentication
if (json['type'] == 'auth') {
// Rate limiting: reject if locked out
if (_authLockoutUntil != null && DateTime.now().isBefore(_authLockoutUntil!)) {
appLogger.w('CompanionRemote: Auth attempt rejected (rate limited)');
socket.add(jsonEncode({'type': 'authFailed', 'message': 'Too many attempts. Try again later.'}));
socket.close(4005, 'Rate limited');
return;
}
final sessionId = json['sessionId'] as String?;
final pin = json['pin'] as String?;
final 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 platform = json['platform'] as String?;
if (sessionId == _sessionId && pin == _pin) {
_failedAuthAttempts = 0;
if (authTag == null ||
clientNonceB64 == null ||
userUUID == null ||
clientIdentifier == null ||
deviceName == null ||
platform == null) {
socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed');
return;
}
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');
@@ -219,45 +283,37 @@ class CompanionRemotePeerService with KeepaliveMixin {
}
_clientSocket = socket;
_sessionEncKey = sessionEncKey;
_sendCounter = 0;
_recvCounter = 0;
_isAuthenticated = true;
appLogger.d('CompanionRemote: Client authenticated: $deviceName ($platform)');
// Send auth success
socket.add(jsonEncode({'type': 'authSuccess'}));
// Send encrypted authSuccess
await _sendEncryptedToSocket(socket, jsonEncode({'type': 'authSuccess'}));
// Notify connection
final device = RemoteDevice(
id: 'remote-client',
name: deviceName ?? 'Unknown Device',
platform: platform ?? 'unknown',
name: deviceName,
platform: platform,
);
_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');
}
} else {
appLogger.w('CompanionRemote: Expected auth, got ${json['type']}');
socket.close(4002, 'Authentication required');
}
} 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);
appLogger.d('CompanionRemote: Received command: ${command.type}');
@@ -280,6 +336,8 @@ class CompanionRemotePeerService with KeepaliveMixin {
appLogger.d('CompanionRemote: WebSocket connection closed');
if (isAuthenticated) {
_clientSocket = null;
_sessionEncKey = null;
_isAuthenticated = false;
_deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected);
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) {
await disconnect();
}
_role = RemoteSessionRole.remote;
_sessionId = sessionId.toUpperCase();
_pin = pin;
_hostAddress = hostAddress;
_myPeerId = 'remote-${Random.secure().nextInt(99999)}';
final completer = Completer<void>();
final auth = RemoteAuthService.instance;
try {
final url = 'ws://$hostAddress/ws';
@@ -321,51 +396,19 @@ class CompanionRemotePeerService with KeepaliveMixin {
_channel = IOWebSocketChannel.connect(Uri.parse(url));
await _channel!.ready;
// Send authentication message
final authMessage = jsonEncode({
'type': 'auth',
'sessionId': _sessionId,
'pin': _pin,
'deviceName': deviceName,
'platform': platform,
});
_channel!.sink.add(authMessage);
List<int>? hostNonce;
List<int>? clientNonce;
String? receivedHostClientId;
// Listen for messages
_channel!.stream.listen(
(data) {
(data) async {
try {
final json = jsonDecode(data as String) as Map<String, dynamic>;
final messageType = json['type'] as String?;
if (_isAuthenticated) {
// Post-auth: all messages are encrypted binary
final decrypted = await _decryptIncoming(data);
if (decrypted == null) return;
if (messageType == 'authSuccess') {
appLogger.d('CompanionRemote: Authentication successful');
if (!completer.isCompleted) {
completer.complete();
}
final device = RemoteDevice(id: 'host', name: 'Desktop', platform: 'desktop');
_deviceConnectedController.add(device);
_connectionStateController.add(RemoteSessionStatus.connected);
// Send device info
sendDeviceInfo(deviceName, platform);
// Start ping timer
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 json = jsonDecode(decrypted) as Map<String, dynamic>;
final command = RemoteCommand.fromJson(json);
appLogger.d('CompanionRemote: Received command: ${command.type}');
@@ -378,6 +421,89 @@ class CompanionRemotePeerService with KeepaliveMixin {
if (command.type == RemoteCommandType.ping) {
_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) {
appLogger.e('CompanionRemote: Failed to parse message', error: e);
@@ -387,8 +513,9 @@ class CompanionRemotePeerService with KeepaliveMixin {
appLogger.d('CompanionRemote: Connection closed');
_deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected);
_isAuthenticated = false;
_sessionEncKey = null;
stopKeepalive();
// Reconnection is handled by CompanionRemoteProvider
},
onError: (error) {
appLogger.e('CompanionRemote: Connection error', error: error);
@@ -422,7 +549,6 @@ class CompanionRemotePeerService with KeepaliveMixin {
return completer.future.timeout(
const Duration(seconds: 15),
onTimeout: () async {
// Clean up channel on timeout
if (_channel != null) {
try {
await _channel!.sink.close();
@@ -435,21 +561,23 @@ class CompanionRemotePeerService with KeepaliveMixin {
}
/// 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(
String sessionId,
String pin,
String deviceName,
String platform,
List<String> hostAddresses,
List<int> homeSecret,
String hostClientId,
String userUUID,
String clientIdentifier,
) async {
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;
}
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 channels = <IOWebSocketChannel>[];
final subs = <StreamSubscription>[];
@@ -471,20 +599,12 @@ class CompanionRemotePeerService with KeepaliveMixin {
final channel = IOWebSocketChannel.connect(Uri.parse(url), connectTimeout: const Duration(seconds: 5));
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(
(data) {
try {
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');
completer.complete(address);
}
@@ -507,11 +627,12 @@ class CompanionRemotePeerService with KeepaliveMixin {
}
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();
// Now set up the proper managed connection on the winning address
await joinSession(sessionId, pin, deviceName, platform, winner);
// Set up the proper managed connection on the winning address
await joinSession(deviceName, platform, winner, homeSecret, hostClientId, userUUID, clientIdentifier);
return winner;
} on TimeoutException {
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
void sendPing() {
if (isConnected) {
@@ -559,18 +723,23 @@ class CompanionRemotePeerService with KeepaliveMixin {
}
void sendCommand(RemoteCommand command) {
if (_sessionEncKey == null || !_isAuthenticated) {
appLogger.w('CompanionRemote: No connection to send command');
return;
}
// 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(json);
appLogger.d('CompanionRemote: Sent command (host): ${command.type}');
_clientSocket!.add(encrypted);
} 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');
_channel!.sink.add(encrypted);
}
appLogger.d('CompanionRemote: Sent command: ${command.type}');
} catch (e) {
appLogger.e('CompanionRemote: Failed to send command', error: e);
_errorController.add(
@@ -581,6 +750,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
),
);
}
});
}
Future<void> disconnect() async {
@@ -607,15 +777,23 @@ class CompanionRemotePeerService with KeepaliveMixin {
_server = null;
}
_sessionId = null;
_pin = null;
_myPeerId = null;
_hostAddress = null;
_role = null;
_sessionEncKey = null;
_sendCounter = 0;
_recvCounter = 0;
_isAuthenticated = false;
_sendChain = null;
_failedAuthAttempts.clear();
_authLockouts.clear();
_connectionStateController.add(RemoteSessionStatus.disconnected);
}
/// Whether the HTTP server is currently running.
bool get isServerRunning => _server != null;
Future<void> dispose() async {
await disconnect();
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 _keyMaxVolume = 'max_volume';
static const String _keyEnableDiscordRPC = 'enable_discord_rpc';
static const String _keyEnableCompanionRemoteServer = 'enable_companion_remote_server';
static const String _keyAutoPip = 'auto_pip';
static const String _keyMatchContentFrameRate = 'match_content_frame_rate';
static const String _keyTunneledPlayback = 'tunneled_playback';
@@ -1104,6 +1105,17 @@ class SettingsService extends BaseSharedPreferencesService {
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)
Future<void> setAutoPip(bool enabled) async {
await prefs.setBool(_keyAutoPip, enabled);
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../i18n/strings.g.dart';
import '../../providers/companion_remote_provider.dart';
import '../../providers/user_profile_provider.dart';
import '../../focus/focusable_button.dart';
import '../../utils/app_logger.dart';
@@ -24,58 +23,55 @@ class RemoteSessionDialog extends StatefulWidget {
}
class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
bool _isCreatingSession = false;
bool _isStarting = false;
String? _errorMessage;
List<String>? _hostAddresses; // Each format: "ip:port"
@override
void initState() {
super.initState();
// Defer to avoid notifyListeners() during build phase
WidgetsBinding.instance.addPostFrameCallback((_) => _createSession());
WidgetsBinding.instance.addPostFrameCallback((_) => _ensureServerRunning());
}
Future<void> _createSession() async {
Future<void> _ensureServerRunning() async {
final provider = context.read<CompanionRemoteProvider>();
if (provider.isHostServerRunning) return;
setState(() {
_isCreatingSession = true;
_isStarting = true;
_errorMessage = null;
_hostAddresses = null;
});
try {
final provider = context.read<CompanionRemoteProvider>();
final result = await provider.createSession();
final home = context.read<UserProfileProvider>().home;
await provider.ensureCryptoReady(home);
if (!mounted) return;
await provider.startHostServer();
if (!mounted) return;
setState(() {
_isCreatingSession = false;
_hostAddresses = result.addresses;
});
if (mounted) setState(() => _isStarting = false);
} 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;
setState(() {
_isCreatingSession = false;
_isStarting = false;
_errorMessage = e.toString();
});
}
}
void _copyToClipboard(String text, String label) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(t.companionRemote.session.copiedToClipboard(label: label)),
duration: const Duration(seconds: 2),
),
);
Future<void> _toggleServer() async {
final provider = context.read<CompanionRemoteProvider>();
if (provider.isHostServerRunning) {
await provider.stopHostServer();
} else {
await _ensureServerRunning();
}
}
@override
Widget build(BuildContext context) {
return Consumer<CompanionRemoteProvider>(
builder: (context, provider, child) {
if (_isCreatingSession) {
if (_isStarting) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(32.0),
@@ -84,7 +80,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
children: [
const CircularProgressIndicator(),
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: [
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(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
@@ -145,16 +125,7 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
children: [
Text(t.companionRemote.title, style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 4),
Text(
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,
),
),
_buildStatusLine(context, provider),
],
),
),
@@ -162,113 +133,27 @@ class _RemoteSessionDialogState extends State<RemoteSessionDialog> {
],
),
const SizedBox(height: 24),
if (session.connectedDevice == null) ...[
Text(
t.companionRemote.session.scanQrCode,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
// Server status card
_buildServerStatus(context, provider),
// Connected device info
if (provider.connectedDevice != null) ...[
const SizedBox(height: 16),
Center(
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,
),
_buildConnectedDevice(context, provider),
],
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (session.connectedDevice != null)
TextButton.icon(
onPressed: () async {
await provider.leaveSession();
await _createSession();
},
icon: const Icon(Icons.refresh),
label: 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),
onPressed: _toggleServer,
icon: Icon(provider.isHostServerRunning ? Icons.stop : Icons.play_arrow),
label: Text(
provider.isHostServerRunning
? t.companionRemote.session.stopServer
: t.companionRemote.session.startServer,
),
),
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(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isRunning ? Colors.green : Colors.grey,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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),
Text(
code,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontFamily: 'monospace', letterSpacing: 2),
isRunning
? 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
version: "3.0.7"
cryptography:
dependency: transitive
dependency: "direct main"
description:
name: cryptography
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
@@ -698,14 +698,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
+1 -1
View File
@@ -43,6 +43,7 @@ dependencies:
drift: ^2.31.0
sqlite3_flutter_libs: ^0.5.42
crypto: ^3.0.7
cryptography: ^2.9.0
file_picker: ^10.3.10
saf_util: ^2.0.0
saf_stream: ^2.0.0
@@ -51,7 +52,6 @@ dependencies:
in_app_review: ^2.0.11
dart_discord_presence: ^1.2.0
flutter_svg: ^2.2.3
mobile_scanner: ^7.2.0
background_downloader: ^9.5.2
sentry_flutter:
git: