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:
wenjie
2026-05-19 15:02:34 +08:00
parent a960417739
commit be7584d908
35 changed files with 713 additions and 41 deletions

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 dimage HDMI',
unsupportedResolution: 'La résolution actuelle nest pas prise en charge',
retrieving: 'Récupération de limage...',
changingResolution: 'Changement de résolution...',
updateFailed: 'Limage ne peut pas être mise à jour pour le moment',
videoError: 'Erreur daffichage vidéo',
noHdmi: 'Aucun signal HDMI détecté',
unavailable: 'Limage ne peut pas être affichée pour le moment'
}
},
keyboard: {
title: 'Clavier',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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: 'キーボード',

View File

@@ -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: '키보드',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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: 'Клавиатура',

View File

@@ -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',

View File

@@ -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: 'คีบอร์ด',

View File

@@ -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',

View File

@@ -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: 'Клавіатура',

View File

@@ -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',

View File

@@ -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: '键盘',

View File

@@ -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: '鍵盤',

View File

@@ -0,0 +1,2 @@
export { CaptureStatusOverlay } from './overlay';
export { useCaptureStatus } from './use-capture-status';

View 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;
}

View 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>
);
}

View 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;
}

View File

@@ -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