mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
feat: show HDMI capture status on desktop screen
Track capture results for each stream mode, broadcast status updates over WebSocket, and display localized warning/error overlays in the desktop view.
This commit is contained in:
@@ -77,7 +77,17 @@ const ca = {
|
||||
frameDetect: 'Detecció de fotogrames',
|
||||
frameDetectTip:
|
||||
"Calcula la diferència entre fotogrames. S'atura la transmissió si no hi ha canvis a la pantalla de l'amfitrió remot.",
|
||||
resetHdmi: 'Restablir HDMI'
|
||||
resetHdmi: 'Restablir HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Error a la pantalla HDMI',
|
||||
unsupportedResolution: 'La resolució actual no és compatible',
|
||||
retrieving: "S'està obtenint la pantalla...",
|
||||
changingResolution: "S'està canviant la resolució...",
|
||||
updateFailed: 'La pantalla no es pot actualitzar ara',
|
||||
videoError: 'Error de visualització de vídeo',
|
||||
noHdmi: "No s'ha detectat cap senyal HDMI",
|
||||
unavailable: 'La pantalla no es pot mostrar ara'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Teclat',
|
||||
@@ -532,6 +542,7 @@ const ca = {
|
||||
success: 'PicoClaw instal·lat correctament',
|
||||
failed: "No s'ha pogut instal·lar PicoClaw",
|
||||
uninstalling: "S'està desinstal·lant el temps d'execució...",
|
||||
uninstaling: "S'està desinstal·lant el temps d'execució...",
|
||||
uninstalled: "El temps d'execució s'ha desinstal·lat correctament.",
|
||||
uninstallFailed: 'La desinstal·lació ha fallat.',
|
||||
requiredTitle: 'PicoClaw no està instal·lat',
|
||||
|
||||
@@ -78,7 +78,17 @@ const cz = {
|
||||
frameDetect: 'Detekce snímků',
|
||||
frameDetectTip:
|
||||
'Vypočítá rozdíl mezi snímky. Přenos video streamu se zastaví, pokud nejsou detekovány změny na obrazovce vzdáleného hostitele.',
|
||||
resetHdmi: 'Resetovat HDMI'
|
||||
resetHdmi: 'Resetovat HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Chyba obrazu HDMI',
|
||||
unsupportedResolution: 'Aktuální rozlišení není podporováno',
|
||||
retrieving: 'Načítá se obraz...',
|
||||
changingResolution: 'Přepíná se rozlišení...',
|
||||
updateFailed: 'Obraz se teď nemůže aktualizovat',
|
||||
videoError: 'Chyba zobrazení videa',
|
||||
noHdmi: 'Nebyl zjištěn signál HDMI',
|
||||
unavailable: 'Obraz teď nelze zobrazit'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Klávesnice',
|
||||
|
||||
@@ -77,7 +77,17 @@ const da = {
|
||||
frameDetect: 'Beregn frames',
|
||||
frameDetectTip:
|
||||
'Beregner forskellen mellem hver frame. Stopper med at sende et video stream hvis der ikke registreres ændringer på fjerncomputerens skærm.',
|
||||
resetHdmi: 'Nulstil HDMI'
|
||||
resetHdmi: 'Nulstil HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Fejl i HDMI-billedet',
|
||||
unsupportedResolution: 'Den aktuelle opløsning understøttes ikke',
|
||||
retrieving: 'Henter skærmbillede...',
|
||||
changingResolution: 'Skifter opløsning...',
|
||||
updateFailed: 'Skærmbilledet kan ikke opdateres lige nu',
|
||||
videoError: 'Fejl i videovisning',
|
||||
noHdmi: 'Intet HDMI-signal registreret',
|
||||
unavailable: 'Skærmbilledet kan ikke vises lige nu'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Tastatur',
|
||||
|
||||
@@ -80,7 +80,17 @@ const de = {
|
||||
frameDetect: 'Bilderkennung',
|
||||
frameDetectTip:
|
||||
'Berechnet den Unterschied zwischen den Einzelbildern. Beendet die Liveübertragung des Videostreams wenn keine Änderungen auf dem Bildschirm des Hosts festgestellt werden kann.',
|
||||
resetHdmi: 'HDMI zurücksetzen'
|
||||
resetHdmi: 'HDMI zurücksetzen',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI-Bildschirmfehler',
|
||||
unsupportedResolution: 'Die aktuelle Auflösung wird nicht unterstützt',
|
||||
retrieving: 'Bildschirm wird abgerufen...',
|
||||
changingResolution: 'Auflösung wird gewechselt...',
|
||||
updateFailed: 'Der Bildschirm kann derzeit nicht aktualisiert werden',
|
||||
videoError: 'Fehler bei der Videoanzeige',
|
||||
noHdmi: 'Kein HDMI-Signal erkannt',
|
||||
unavailable: 'Der Bildschirm kann derzeit nicht angezeigt werden'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Tastatur',
|
||||
|
||||
@@ -76,7 +76,17 @@ const en = {
|
||||
frameDetect: 'Frame Detect',
|
||||
frameDetectTip:
|
||||
"Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.",
|
||||
resetHdmi: 'Reset HDMI'
|
||||
resetHdmi: 'Reset HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI screen error',
|
||||
unsupportedResolution: 'Current resolution is not supported',
|
||||
retrieving: 'Getting screen...',
|
||||
changingResolution: 'Switching resolution...',
|
||||
updateFailed: 'Screen cannot update right now',
|
||||
videoError: 'Video display error',
|
||||
noHdmi: 'No HDMI signal detected',
|
||||
unavailable: 'Screen cannot be displayed right now'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Keyboard',
|
||||
|
||||
@@ -78,7 +78,17 @@ const es = {
|
||||
frameDetect: 'Detectar fotogramas',
|
||||
frameDetectTip:
|
||||
'Calcula la diferencia entre fotogramas. Para de transmitir vídeo cuando no se detectan cambios en la pantalla del host remoto.',
|
||||
resetHdmi: 'Reiniciar HDMI'
|
||||
resetHdmi: 'Reiniciar HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Error de imagen HDMI',
|
||||
unsupportedResolution: 'La resolución actual no es compatible',
|
||||
retrieving: 'Obteniendo pantalla...',
|
||||
changingResolution: 'Cambiando resolución...',
|
||||
updateFailed: 'La pantalla no puede actualizarse ahora',
|
||||
videoError: 'Error de visualización de video',
|
||||
noHdmi: 'No se detectó señal HDMI',
|
||||
unavailable: 'La pantalla no puede mostrarse ahora'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Teclado',
|
||||
|
||||
@@ -80,7 +80,17 @@ const fr = {
|
||||
frameDetect: 'Détection de trame',
|
||||
frameDetectTip:
|
||||
"Calcule la différence entre les images. Arrête la transmission du flux vidéo lorsqu'aucun changement n'est détecté sur l'écran de l'hôte distant",
|
||||
resetHdmi: 'Réinitialiser le HDMI'
|
||||
resetHdmi: 'Réinitialiser le HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Erreur d’image HDMI',
|
||||
unsupportedResolution: 'La résolution actuelle n’est pas prise en charge',
|
||||
retrieving: 'Récupération de l’image...',
|
||||
changingResolution: 'Changement de résolution...',
|
||||
updateFailed: 'L’image ne peut pas être mise à jour pour le moment',
|
||||
videoError: 'Erreur d’affichage vidéo',
|
||||
noHdmi: 'Aucun signal HDMI détecté',
|
||||
unavailable: 'L’image ne peut pas être affichée pour le moment'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Clavier',
|
||||
|
||||
@@ -79,7 +79,17 @@ const hu = {
|
||||
frameDetect: 'Képkocka-figyelés',
|
||||
frameDetectTip:
|
||||
'Elemzi a képkockák közötti különbségeket. A videó stream küldése leáll, ha a távoli gép képernyőjén nem történik változás.',
|
||||
resetHdmi: 'HDMI visszaállítása'
|
||||
resetHdmi: 'HDMI visszaállítása',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI-képernyőhiba',
|
||||
unsupportedResolution: 'A jelenlegi felbontás nem támogatott',
|
||||
retrieving: 'Kép lekérése...',
|
||||
changingResolution: 'Felbontás váltása...',
|
||||
updateFailed: 'A kép jelenleg nem frissíthető',
|
||||
videoError: 'Videómegjelenítési hiba',
|
||||
noHdmi: 'Nem észlelhető HDMI-jel',
|
||||
unavailable: 'A kép jelenleg nem jeleníthető meg'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Billentyűzet',
|
||||
|
||||
@@ -77,7 +77,17 @@ const id = {
|
||||
frameDetect: 'Deteksi bingkai',
|
||||
frameDetectTip:
|
||||
'Hitung selisih antar frame. Hentikan transmisi aliran video saat tidak ada perubahan yang terdeteksi di layar host jarak jauh.',
|
||||
resetHdmi: 'Atur ulang HDMI'
|
||||
resetHdmi: 'Atur ulang HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Kesalahan layar HDMI',
|
||||
unsupportedResolution: 'Resolusi saat ini tidak didukung',
|
||||
retrieving: 'Mengambil layar...',
|
||||
changingResolution: 'Mengganti resolusi...',
|
||||
updateFailed: 'Layar tidak dapat diperbarui saat ini',
|
||||
videoError: 'Kesalahan tampilan video',
|
||||
noHdmi: 'Sinyal HDMI tidak terdeteksi',
|
||||
unavailable: 'Layar tidak dapat ditampilkan saat ini'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Keyboard',
|
||||
|
||||
@@ -79,7 +79,17 @@ const it = {
|
||||
frameDetect: 'Rilevamento Frame',
|
||||
frameDetectTip:
|
||||
'Calcola la differenza tra i frame. Interrompe la trasmissione del flusso video quando non vengono rilevate modifiche sullo schermo del dispositivo remoto.',
|
||||
resetHdmi: 'Reimposta HDMI'
|
||||
resetHdmi: 'Reimposta HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Errore schermata HDMI',
|
||||
unsupportedResolution: 'La risoluzione attuale non è supportata',
|
||||
retrieving: 'Acquisizione schermata...',
|
||||
changingResolution: 'Cambio risoluzione...',
|
||||
updateFailed: 'Lo schermo non può aggiornarsi al momento',
|
||||
videoError: 'Errore di visualizzazione video',
|
||||
noHdmi: 'Nessun segnale HDMI rilevato',
|
||||
unavailable: 'Lo schermo non può essere visualizzato al momento'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Tastiera',
|
||||
|
||||
@@ -77,7 +77,17 @@ const ja = {
|
||||
frameDetect: 'フレーム差分検出',
|
||||
frameDetectTip:
|
||||
'フレーム間の差異を計算し、リモートホストの画面が変更されない場合はビデオストリームの送信を停止します',
|
||||
resetHdmi: 'HDMI をリセット'
|
||||
resetHdmi: 'HDMI をリセット',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI 画面エラー',
|
||||
unsupportedResolution: '現在の解像度はサポートされていません',
|
||||
retrieving: '画面を取得中...',
|
||||
changingResolution: '解像度を切り替え中...',
|
||||
updateFailed: '現在、画面を更新できません',
|
||||
videoError: '映像表示エラー',
|
||||
noHdmi: 'HDMI 信号が検出されません',
|
||||
unavailable: '現在、画面を表示できません'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'キーボード',
|
||||
|
||||
@@ -76,7 +76,17 @@ const ko = {
|
||||
frameDetect: '프레임 탐지',
|
||||
frameDetectTip:
|
||||
'프레임 간의 차이를 계산합니다. 원격 호스트 화면에 변경 사항이 감지되지 않으면 비디오 스트림 전송을 중지합니다.',
|
||||
resetHdmi: 'HDMI 초기화'
|
||||
resetHdmi: 'HDMI 초기화',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI 화면 오류',
|
||||
unsupportedResolution: '현재 해상도는 지원되지 않습니다',
|
||||
retrieving: '화면을 가져오는 중...',
|
||||
changingResolution: '해상도를 전환하는 중...',
|
||||
updateFailed: '지금은 화면을 업데이트할 수 없습니다',
|
||||
videoError: '비디오 표시 오류',
|
||||
noHdmi: 'HDMI 신호가 감지되지 않았습니다',
|
||||
unavailable: '지금은 화면을 표시할 수 없습니다'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: '키보드',
|
||||
|
||||
@@ -78,7 +78,17 @@ const nb = {
|
||||
frameDetect: 'Bildefrekvensoppdagelse',
|
||||
frameDetectTip:
|
||||
'Kalkuler forskjellen mellom bilder. Stopper overføring av video når det ikke oppdages forskjell på den eksterne vertens skjerm.',
|
||||
resetHdmi: 'Tilbakestill HDMI'
|
||||
resetHdmi: 'Tilbakestill HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI-skjermfeil',
|
||||
unsupportedResolution: 'Gjeldende oppløsning støttes ikke',
|
||||
retrieving: 'Henter skjermbilde...',
|
||||
changingResolution: 'Bytter oppløsning...',
|
||||
updateFailed: 'Skjermen kan ikke oppdateres akkurat nå',
|
||||
videoError: 'Feil ved videovisning',
|
||||
noHdmi: 'Ingen HDMI-signal oppdaget',
|
||||
unavailable: 'Skjermen kan ikke vises akkurat nå'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Åpne tastatur',
|
||||
|
||||
@@ -79,7 +79,17 @@ const nl = {
|
||||
frameDetect: 'Frame detectie',
|
||||
frameDetectTip:
|
||||
'Berekent het verschil tussen frames. Stopt met het verzenden van de videostream wanneer er geen veranderingen worden gedetecteerd op het scherm van de externe host.',
|
||||
resetHdmi: 'Reset HDMI'
|
||||
resetHdmi: 'Reset HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI-schermfout',
|
||||
unsupportedResolution: 'De huidige resolutie wordt niet ondersteund',
|
||||
retrieving: 'Scherm ophalen...',
|
||||
changingResolution: 'Resolutie wisselen...',
|
||||
updateFailed: 'Het scherm kan nu niet worden bijgewerkt',
|
||||
videoError: 'Fout bij videoweergave',
|
||||
noHdmi: 'Geen HDMI-signaal gedetecteerd',
|
||||
unavailable: 'Het scherm kan nu niet worden weergegeven'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Toetsenbord',
|
||||
|
||||
@@ -78,7 +78,17 @@ const pl = {
|
||||
frameDetect: 'Wykrywanie klatek',
|
||||
frameDetectTip:
|
||||
'Obliczanie różnicy między klatkami. Zatrzymaj transmisję strumienia wideo, gdy na ekranie zdalnego hosta nie zostaną wykryte żadne zmiany.',
|
||||
resetHdmi: 'Resetuj HDMI'
|
||||
resetHdmi: 'Resetuj HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Błąd obrazu HDMI',
|
||||
unsupportedResolution: 'Bieżąca rozdzielczość nie jest obsługiwana',
|
||||
retrieving: 'Pobieranie obrazu...',
|
||||
changingResolution: 'Przełączanie rozdzielczości...',
|
||||
updateFailed: 'Nie można teraz zaktualizować obrazu',
|
||||
videoError: 'Błąd wyświetlania wideo',
|
||||
noHdmi: 'Nie wykryto sygnału HDMI',
|
||||
unavailable: 'Nie można teraz wyświetlić obrazu'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Klawiatura',
|
||||
|
||||
@@ -77,7 +77,17 @@ const pt_br = {
|
||||
frameDetect: 'Detecção de Quadros',
|
||||
frameDetectTip:
|
||||
'Calcular a diferença entre os quadros. Parar a transmissão de vídeo quando nenhuma alteração for detectada na tela do host remoto.',
|
||||
resetHdmi: 'Redefinir HDMI'
|
||||
resetHdmi: 'Redefinir HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Erro na imagem HDMI',
|
||||
unsupportedResolution: 'A resolução atual não é compatível',
|
||||
retrieving: 'Obtendo tela...',
|
||||
changingResolution: 'Alterando resolução...',
|
||||
updateFailed: 'A tela não pode ser atualizada agora',
|
||||
videoError: 'Erro na exibição de vídeo',
|
||||
noHdmi: 'Nenhum sinal HDMI detectado',
|
||||
unavailable: 'A tela não pode ser exibida agora'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Teclado',
|
||||
|
||||
@@ -77,7 +77,17 @@ const ru = {
|
||||
frameDetect: 'Экономия трафика',
|
||||
frameDetectTip:
|
||||
'Вычисляет разницу между кадрами и прекращает передачу видеопотока, если на экране удаленного узла не обнаружено никаких изменений.',
|
||||
resetHdmi: 'Перезагрузить HDMI подсистему'
|
||||
resetHdmi: 'Перезагрузить HDMI подсистему',
|
||||
captureStatus: {
|
||||
hdmiError: 'Ошибка изображения HDMI',
|
||||
unsupportedResolution: 'Текущее разрешение не поддерживается',
|
||||
retrieving: 'Получение изображения...',
|
||||
changingResolution: 'Переключение разрешения...',
|
||||
updateFailed: 'Сейчас невозможно обновить изображение',
|
||||
videoError: 'Ошибка отображения видео',
|
||||
noHdmi: 'Сигнал HDMI не обнаружен',
|
||||
unavailable: 'Сейчас невозможно отобразить изображение'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Клавиатура',
|
||||
|
||||
@@ -75,7 +75,17 @@ const se = {
|
||||
frameDetect: 'Ramdetection',
|
||||
frameDetectTip:
|
||||
'Beräkna skillnaden mellan ramar. Sluta skicka videoström när inga förändringar upptäcks på fjärrvärdens skärm.',
|
||||
resetHdmi: 'Återställ HDMI'
|
||||
resetHdmi: 'Återställ HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI-skärmfel',
|
||||
unsupportedResolution: 'Den aktuella upplösningen stöds inte',
|
||||
retrieving: 'Hämtar skärmen...',
|
||||
changingResolution: 'Byter upplösning...',
|
||||
updateFailed: 'Skärmen kan inte uppdateras just nu',
|
||||
videoError: 'Videovisningsfel',
|
||||
noHdmi: 'Ingen HDMI-signal upptäcktes',
|
||||
unavailable: 'Skärmen kan inte visas just nu'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Tangentbord',
|
||||
|
||||
@@ -75,7 +75,17 @@ const th = {
|
||||
frameDetect: 'ตรวจจับเฟรม',
|
||||
frameDetectTip:
|
||||
'ระบบจะคำนวณความแตกต่างระหว่างเฟรม และหยุดส่งสตรีมวิดีโอเมื่อไม่พบการเปลี่ยนแปลงบนหน้าจอของคอมพิวเตอร์ต้นทาง',
|
||||
resetHdmi: 'รีเช็ท HDMI'
|
||||
resetHdmi: 'รีเช็ท HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'ข้อผิดพลาดหน้าจอ HDMI',
|
||||
unsupportedResolution: 'ไม่รองรับความละเอียดปัจจุบัน',
|
||||
retrieving: 'กำลังดึงหน้าจอ...',
|
||||
changingResolution: 'กำลังเปลี่ยนความละเอียด...',
|
||||
updateFailed: 'ไม่สามารถอัปเดตหน้าจอได้ในขณะนี้',
|
||||
videoError: 'ข้อผิดพลาดในการแสดงวิดีโอ',
|
||||
noHdmi: 'ไม่พบสัญญาณ HDMI',
|
||||
unavailable: 'ไม่สามารถแสดงหน้าจอได้ในขณะนี้'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'คีบอร์ด',
|
||||
|
||||
@@ -78,7 +78,17 @@ const tr = {
|
||||
frameDetect: 'Kareleri algıla',
|
||||
frameDetectTip:
|
||||
'Gönderilen kareler arasındaki farkı hesaplar. Uzak ana bilgisayardan gönderilen yayında bir değişiklik yoksa görüntü yayınını durdurur.',
|
||||
resetHdmi: 'HDMI sıfırla'
|
||||
resetHdmi: 'HDMI sıfırla',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI ekran hatası',
|
||||
unsupportedResolution: 'Geçerli çözünürlük desteklenmiyor',
|
||||
retrieving: 'Ekran alınıyor...',
|
||||
changingResolution: 'Çözünürlük değiştiriliyor...',
|
||||
updateFailed: 'Ekran şu anda güncellenemiyor',
|
||||
videoError: 'Video görüntüleme hatası',
|
||||
noHdmi: 'HDMI sinyali algılanmadı',
|
||||
unavailable: 'Ekran şu anda gösterilemiyor'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Klavye',
|
||||
|
||||
@@ -78,7 +78,17 @@ const uk = {
|
||||
frameDetect: 'Виявлення кадрів',
|
||||
frameDetectTip:
|
||||
'Обчислює різницю між кадрами. Зупиняє передачу відеопотоку, коли на екрані віддаленого хоста не виявлено змін.',
|
||||
resetHdmi: 'Перезавантажити HDMI підсистему'
|
||||
resetHdmi: 'Перезавантажити HDMI підсистему',
|
||||
captureStatus: {
|
||||
hdmiError: 'Помилка зображення HDMI',
|
||||
unsupportedResolution: 'Поточна роздільна здатність не підтримується',
|
||||
retrieving: 'Отримання зображення...',
|
||||
changingResolution: 'Перемикання роздільної здатності...',
|
||||
updateFailed: 'Зараз неможливо оновити зображення',
|
||||
videoError: 'Помилка відображення відео',
|
||||
noHdmi: 'Сигнал HDMI не виявлено',
|
||||
unavailable: 'Зараз неможливо показати зображення'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Клавіатура',
|
||||
|
||||
@@ -77,7 +77,17 @@ const vi = {
|
||||
frameDetect: 'Phát hiện khung hình',
|
||||
frameDetectTip:
|
||||
'Tính toán sự khác biệt giữa các khung hình. Dừng truyền video khi không có thay đổi trên màn hình máy chủ từ xa.',
|
||||
resetHdmi: 'Đặt lại HDMI'
|
||||
resetHdmi: 'Đặt lại HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'Lỗi màn hình HDMI',
|
||||
unsupportedResolution: 'Độ phân giải hiện tại không được hỗ trợ',
|
||||
retrieving: 'Đang lấy màn hình...',
|
||||
changingResolution: 'Đang chuyển độ phân giải...',
|
||||
updateFailed: 'Hiện không thể cập nhật màn hình',
|
||||
videoError: 'Lỗi hiển thị video',
|
||||
noHdmi: 'Không phát hiện tín hiệu HDMI',
|
||||
unavailable: 'Hiện không thể hiển thị màn hình'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Bàn phím',
|
||||
|
||||
@@ -73,7 +73,17 @@ const zh = {
|
||||
qualityLow: '低',
|
||||
frameDetect: '帧差检测',
|
||||
frameDetectTip: '计算帧之间的差异,当检测到远程主机画面不变时,停止传输视频流',
|
||||
resetHdmi: '重置 HDMI'
|
||||
resetHdmi: '重置 HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI 画面异常',
|
||||
unsupportedResolution: '当前分辨率不支持',
|
||||
retrieving: '正在获取画面...',
|
||||
changingResolution: '正在切换分辨率...',
|
||||
updateFailed: '画面暂时无法更新',
|
||||
videoError: '视频显示异常',
|
||||
noHdmi: '未检测到 HDMI 信号',
|
||||
unavailable: '画面暂时无法显示'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: '键盘',
|
||||
|
||||
@@ -73,7 +73,17 @@ const zh_tw = {
|
||||
qualityLow: '低',
|
||||
frameDetect: '影格檢測',
|
||||
frameDetectTip: '計算影格之間的差異。當遠端主機畫面未偵測到任何變更時,停止視訊傳輸串流。',
|
||||
resetHdmi: '重置 HDMI'
|
||||
resetHdmi: '重置 HDMI',
|
||||
captureStatus: {
|
||||
hdmiError: 'HDMI 畫面異常',
|
||||
unsupportedResolution: '目前解析度不支援',
|
||||
retrieving: '正在取得畫面...',
|
||||
changingResolution: '正在切換解析度...',
|
||||
updateFailed: '畫面暫時無法更新',
|
||||
videoError: '影片顯示異常',
|
||||
noHdmi: '未偵測到 HDMI 訊號',
|
||||
unavailable: '畫面暫時無法顯示'
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
title: '鍵盤',
|
||||
|
||||
2
web/src/pages/desktop/capture-status/index.ts
Normal file
2
web/src/pages/desktop/capture-status/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { CaptureStatusOverlay } from './overlay';
|
||||
export { useCaptureStatus } from './use-capture-status';
|
||||
74
web/src/pages/desktop/capture-status/model.ts
Normal file
74
web/src/pages/desktop/capture-status/model.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
export const CAPTURE_STATUS_EVENT = 'capture-status';
|
||||
|
||||
export type CaptureStatus = {
|
||||
ok: boolean;
|
||||
result: number;
|
||||
message: string;
|
||||
mode: string;
|
||||
severity: 'error' | 'warning' | '';
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function parseCaptureStatusMessage(message: { data: unknown }): CaptureStatus | null {
|
||||
if (typeof message.data !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const envelope = JSON.parse(message.data) as { data?: unknown };
|
||||
if (typeof envelope.data !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = JSON.parse(envelope.data) as Partial<CaptureStatus>;
|
||||
if (typeof status.ok !== 'boolean' || typeof status.result !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: status.ok,
|
||||
result: status.result,
|
||||
message: typeof status.message === 'string' ? status.message : '',
|
||||
mode: typeof status.mode === 'string' ? status.mode : '',
|
||||
severity: status.severity === 'warning' ? 'warning' : status.ok ? '' : 'error',
|
||||
updatedAt: typeof status.updatedAt === 'string' ? status.updatedAt : ''
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCaptureStatusMessageKey(result: number) {
|
||||
switch (result) {
|
||||
case -7:
|
||||
return 'screen.captureStatus.hdmiError';
|
||||
case -6:
|
||||
return 'screen.captureStatus.unsupportedResolution';
|
||||
case -5:
|
||||
return 'screen.captureStatus.retrieving';
|
||||
case -4:
|
||||
return 'screen.captureStatus.changingResolution';
|
||||
case -3:
|
||||
return 'screen.captureStatus.updateFailed';
|
||||
case -2:
|
||||
return 'screen.captureStatus.videoError';
|
||||
case -1:
|
||||
return 'screen.captureStatus.noHdmi';
|
||||
default:
|
||||
return result < 0 ? 'screen.captureStatus.unavailable' : '';
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldAcceptCaptureStatus(current: CaptureStatus | null, next: CaptureStatus) {
|
||||
if (!current) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return getCaptureStatusTimestamp(next) >= getCaptureStatusTimestamp(current);
|
||||
}
|
||||
|
||||
function getCaptureStatusTimestamp(status: CaptureStatus) {
|
||||
const timestamp = Date.parse(status.updatedAt);
|
||||
return Number.isFinite(timestamp) ? timestamp : 0;
|
||||
}
|
||||
30
web/src/pages/desktop/capture-status/overlay.tsx
Normal file
30
web/src/pages/desktop/capture-status/overlay.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AlertCircle, AlertTriangle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { CaptureStatus, getCaptureStatusMessageKey } from './model';
|
||||
|
||||
export function CaptureStatusOverlay({ status }: { status: CaptureStatus | null }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!status || status.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messageKey = getCaptureStatusMessageKey(status.result);
|
||||
const isWarning = status.severity === 'warning';
|
||||
|
||||
const toneClass = isWarning
|
||||
? 'border-amber-500/50 bg-neutral-900/90 text-amber-400 shadow-amber-900/20'
|
||||
: 'border-red-500/50 bg-neutral-900/90 text-red-400 shadow-red-900/20';
|
||||
|
||||
const Icon = isWarning ? AlertTriangle : AlertCircle;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`pointer-events-none absolute left-1/2 top-1/2 z-10 flex max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 items-center gap-2.5 rounded-lg border px-4 py-2.5 text-sm font-medium shadow-xl backdrop-blur-md ${toneClass}`}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0 animate-pulse" />
|
||||
<span className="min-w-0 truncate">{t(messageKey)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
web/src/pages/desktop/capture-status/use-capture-status.ts
Normal file
39
web/src/pages/desktop/capture-status/use-capture-status.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
|
||||
import {
|
||||
CAPTURE_STATUS_EVENT,
|
||||
CaptureStatus,
|
||||
parseCaptureStatusMessage,
|
||||
shouldAcceptCaptureStatus
|
||||
} from './model';
|
||||
|
||||
export function useCaptureStatus(activeVideoMode: string) {
|
||||
const [captureStatus, setCaptureStatus] = useState<CaptureStatus | null>(null);
|
||||
const latestStatusRef = useRef<CaptureStatus | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
latestStatusRef.current = null;
|
||||
setCaptureStatus(null);
|
||||
|
||||
if (!activeVideoMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
return client.on(CAPTURE_STATUS_EVENT, (message) => {
|
||||
const status = parseCaptureStatusMessage(message);
|
||||
if (!status || status.mode !== activeVideoMode) {
|
||||
return;
|
||||
}
|
||||
if (!shouldAcceptCaptureStatus(latestStatusRef.current, status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
latestStatusRef.current = status;
|
||||
setCaptureStatus(status.ok ? null : status);
|
||||
});
|
||||
}, [activeVideoMode]);
|
||||
|
||||
return captureStatus;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { picoclawChatOpenAtom } from '@/jotai/picoclaw.ts';
|
||||
import { resolutionAtom, videoModeAtom } from '@/jotai/screen.ts';
|
||||
import { Head } from '@/components/head.tsx';
|
||||
|
||||
import { CaptureStatusOverlay, useCaptureStatus } from './capture-status';
|
||||
import { Keyboard } from './keyboard';
|
||||
import { Menu } from './menu';
|
||||
import { Mouse } from './mouse';
|
||||
@@ -19,10 +20,26 @@ import { ActionOverlay } from './picoclaw/action-overlay.tsx';
|
||||
import { Screen } from './screen';
|
||||
import { VirtualKeyboard } from './virtual-keyboard';
|
||||
|
||||
function getVideoMode() {
|
||||
const defaultVideoMode = window.RTCPeerConnection ? 'h264' : 'mjpeg';
|
||||
|
||||
const cookieVideoMode = storage.getVideoMode();
|
||||
if (cookieVideoMode) {
|
||||
if (cookieVideoMode === 'direct' && !window.VideoDecoder) {
|
||||
return defaultVideoMode;
|
||||
}
|
||||
return cookieVideoMode;
|
||||
}
|
||||
|
||||
return defaultVideoMode;
|
||||
}
|
||||
|
||||
export const Desktop = () => {
|
||||
const { t } = useTranslation();
|
||||
const isBigScreen = useMediaQuery({ minWidth: 850 });
|
||||
const [activeVideoMode] = useState(getVideoMode);
|
||||
const [picoclawSidebarWidth, setPicoclawSidebarWidth] = useState(420);
|
||||
const captureStatus = useCaptureStatus(activeVideoMode);
|
||||
|
||||
const [videoMode, setVideoMode] = useAtom(videoModeAtom);
|
||||
const [resolution, setResolution] = useAtom(resolutionAtom);
|
||||
@@ -31,8 +48,7 @@ export const Desktop = () => {
|
||||
useEffect(() => {
|
||||
client.connect();
|
||||
|
||||
const mode = getVideoMode();
|
||||
setVideoMode(mode);
|
||||
setVideoMode(activeVideoMode);
|
||||
|
||||
const res = storage.getResolution() || { width: 0, height: 0 };
|
||||
setResolution(res);
|
||||
@@ -40,21 +56,7 @@ export const Desktop = () => {
|
||||
return () => {
|
||||
client.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function getVideoMode() {
|
||||
const defaultVideoMode = window.RTCPeerConnection ? 'h264' : 'mjpeg';
|
||||
|
||||
const cookieVideoMode = storage.getVideoMode();
|
||||
if (cookieVideoMode) {
|
||||
if (cookieVideoMode === 'direct' && !window.VideoDecoder) {
|
||||
return defaultVideoMode;
|
||||
}
|
||||
return cookieVideoMode;
|
||||
}
|
||||
|
||||
return defaultVideoMode;
|
||||
}
|
||||
}, [activeVideoMode, setResolution, setVideoMode]);
|
||||
|
||||
function handleSplitterResize(sizes: number[]) {
|
||||
const nextSidebarWidth = sizes[1];
|
||||
@@ -81,6 +83,7 @@ export const Desktop = () => {
|
||||
<Splitter.Panel min="45%">
|
||||
<div className="relative h-full min-h-0 w-full min-w-0 overflow-hidden bg-black">
|
||||
<Screen />
|
||||
<CaptureStatusOverlay status={captureStatus} />
|
||||
</div>
|
||||
</Splitter.Panel>
|
||||
<Splitter.Panel
|
||||
|
||||
Reference in New Issue
Block a user