diff --git a/packages/ui/src/apps/MobileApp.tsx b/packages/ui/src/apps/MobileApp.tsx index 25940538..2d1e58c4 100644 --- a/packages/ui/src/apps/MobileApp.tsx +++ b/packages/ui/src/apps/MobileApp.tsx @@ -61,6 +61,7 @@ import { MobileSessionsSheet } from './MobileSessionsSheet'; import { MobileSurfaceShell } from './MobileSurfaceShell'; import { DedicatedMobileAppProvider, type MobileAppActions } from './mobileAppContext'; import { autoConnectLastInstance, connectionDisplayUrl, isActiveRuntimeConnection, reprobeActiveConnection, useMobileConnection } from './mobileConnections'; +import { isRelayModeActive } from '@/lib/relay/runtime-tunnel'; import { isQrScanSupported, parseConnectionPayload, scanConnectionQr } from './mobileQrScan'; import { reconnectAppForTransportSwitch, resetAppForRuntimeEndpointChange } from './runtimeEndpointReset'; import { useAppFontEffects } from './useAppFontEffects'; @@ -682,8 +683,12 @@ const MobileConnectionWelcome: React.FC<{ onConnected: () => void }> = ({ onConn const [connectionName, setConnectionName] = React.useState(''); const [clientToken, setClientToken] = React.useState(''); const [isScanning, setIsScanning] = React.useState(false); - const [advancedOpen, setAdvancedOpen] = React.useState(false); const qrScanSupported = React.useMemo(() => isQrScanSupported(), []); + // QR pairing is the primary flow; the manual URL form stays collapsed unless + // scanning is unavailable (web build) or the user asks for it. + const [manualOpen, setManualOpen] = React.useState(() => !isQrScanSupported()); + // Which saved connection is being connected to, for the per-row spinner. + const [connectingId, setConnectingId] = React.useState(null); const [password, setPassword] = React.useState(''); const handleSubmit = React.useCallback((event: React.FormEvent) => { @@ -692,7 +697,7 @@ const MobileConnectionWelcome: React.FC<{ onConnected: () => void }> = ({ onConn }, [clientToken, conn, connectionName, serverUrl]); // Accept a pasted pairing link (openchamber://connect?...) in the URL field and - // split it back into the server URL + token, revealing the token field when present. + // split it back into the server URL + token. const handleUrlChange = React.useCallback((value: string) => { if (/^openchamber:\/\//i.test(value.trim())) { const payload = parseConnectionPayload(value); @@ -704,7 +709,6 @@ const MobileConnectionWelcome: React.FC<{ onConnected: () => void }> = ({ onConn setServerUrl(payload.url); if (payload.label) setConnectionName(payload.label); if (payload.clientToken) setClientToken(payload.clientToken); - if (payload.label || payload.clientToken) setAdvancedOpen(true); return; } } @@ -722,7 +726,6 @@ const MobileConnectionWelcome: React.FC<{ onConnected: () => void }> = ({ onConn setServerUrl(result.url); if (result.label) setConnectionName(result.label); if (result.clientToken) setClientToken(result.clientToken); - if (result.label || result.clientToken) setAdvancedOpen(true); await conn.connect({ url: result.url, clientToken: result.clientToken, label: result.label }); break; case 'pairing': @@ -805,119 +808,133 @@ const MobileConnectionWelcome: React.FC<{ onConnected: () => void }> = ({ onConn ) : ( -
-
- setConnectionName(event.target.value)} - placeholder={t('mobile.instances.label.placeholder')} - aria-label={t('mobile.instances.label.label')} - autoComplete="off" - autoCapitalize="words" - autoCorrect="off" - spellCheck={false} - className="h-12 w-full rounded-[16px] border border-border/70 bg-surface-elevated px-4 text-center text-[16px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20" - /> - handleUrlChange(event.target.value)} - placeholder={t('mobile.connect.url.placeholder')} - aria-label={t('mobile.connect.url.label')} - type="url" - inputMode="url" - autoCapitalize="none" - className="h-12 w-full rounded-[16px] border border-border/70 bg-surface-elevated px-4 text-center text-[16px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20" - /> +
+ {/* Primary path: scan the pairing QR from "Add a device" on the server. */} + {qrScanSupported ? ( +
+ +

+ {t('mobile.connect.welcome.scanHint')} +

+
+ ) : null} -
+ {error && !manualOpen ?

{error}

: null} + + {connections.length > 0 ? ( +
+

+ {t('mobile.connect.saved.title')} +

+
+ {connections.map((connection) => { + const isConnectingRow = connectingId === connection.id; + return ( + + ); + })} +
+
+ ) : null} + + {/* Manual URL entry, collapsed by default — most people pair by QR. */} +
+ {qrScanSupported ? ( -
-
-
- -

{t('mobile.connect.token.hint')}

-
-
+ ) : null} +
+
+ + handleUrlChange(event.target.value)} + placeholder={t('mobile.connect.url.placeholder')} + aria-label={t('mobile.connect.url.label')} + type="url" + inputMode="url" + autoCapitalize="none" + tabIndex={manualOpen ? undefined : -1} + className="h-12 w-full rounded-[16px] border border-border/70 bg-surface-elevated px-4 text-center text-[16px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20" + /> + setConnectionName(event.target.value)} + placeholder={t('mobile.instances.label.placeholder')} + aria-label={t('mobile.instances.label.label')} + autoComplete="off" + autoCapitalize="words" + autoCorrect="off" + spellCheck={false} + tabIndex={manualOpen ? undefined : -1} + className="h-12 w-full rounded-[16px] border border-border/70 bg-surface-elevated px-4 text-center text-[16px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20" + /> + setClientToken(event.target.value)} + placeholder={t('mobile.connect.token.placeholder')} + aria-label={t('mobile.connect.token.label')} + tabIndex={manualOpen ? undefined : -1} + autoCapitalize="none" + className="h-12 w-full rounded-[16px] border border-border/70 bg-surface-elevated px-4 text-center text-[16px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20" + /> +

{t('mobile.connect.token.hint')}

+ {error ?

{error}

: null} + +
- - {error ?

{error}

: null} - - - - - - {!qrScanSupported ? ( -

- {t('mobile.connect.scan.unsupported')} -

- ) : null} +
)} - - {!pendingConnection && connections.length > 0 ? ( -
-

- {t('mobile.connect.saved.title')} -

-
- {connections.map((connection) => ( - - ))} -
-
- ) : null}
); @@ -942,6 +959,11 @@ const MobileInstancesSurface: React.FC<{ const [password, setPassword] = React.useState(''); const [isScanning, setIsScanning] = React.useState(false); const qrScanSupported = React.useMemo(() => isQrScanSupported(), []); + // The manual add/edit form is hidden until asked for — the sheet leads with + // the list of instances (with live status), not a wall of inputs. + const [formOpen, setFormOpen] = React.useState(false); + // Which row is being connected to, for the per-row spinner. + const [connectingId, setConnectingId] = React.useState(null); // Populate/clear the form imperatively (on edit tap / cancel / save) rather than via // an effect keyed on the derived connection object. With an effect, any churn of the @@ -953,6 +975,7 @@ const MobileInstancesSurface: React.FC<{ setLabel(''); setClientToken(''); setError(null); + setFormOpen(false); }, [setError]); const saveInstance = React.useCallback((event: React.FormEvent) => { @@ -972,9 +995,11 @@ const MobileInstancesSurface: React.FC<{ const result = await scanConnectionQr(); switch (result.status) { case 'ok': + // Legacy token QR: prefill the manual form for review before saving. setUrl(result.url); if (result.label) setLabel(result.label); if (result.clientToken) setClientToken(result.clientToken); + setFormOpen(true); break; case 'pairing': await conn.redeemPairingConnection(result.pairing); @@ -1019,13 +1044,16 @@ const MobileInstancesSurface: React.FC<{ const confirmDelete = React.useCallback((id: string) => { setConfirmingDeleteId(null); if (editingId === id) resetForm(); + // Removing the ACTIVE instance — or the LAST one — must drop the user back + // to the connect screen instead of leaving them in a stale, unbacked UI. + const wasLast = connections.length === 1; void removeConnection(id).then((removed) => { if (!removed) return; - if (isActiveRuntimeConnection(removed)) { + if (wasLast || isActiveRuntimeConnection(removed)) { onActiveConnectionDeleted(); } }); - }, [editingId, onActiveConnectionDeleted, removeConnection, resetForm]); + }, [connections.length, editingId, onActiveConnectionDeleted, removeConnection, resetForm]); const inputClass = 'h-12 w-full rounded-[16px] border border-border/70 bg-surface-elevated px-4 text-[16px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20'; @@ -1071,11 +1099,20 @@ const MobileInstancesSurface: React.FC<{ return (
-
+
{connections.length > 0 ? (
{connections.map((connection) => { const confirming = confirmingDeleteId === connection.id; + const isActive = isActiveRuntimeConnection(connection); + const isConnectingRow = connectingId === connection.id; + // Status line: the active instance says HOW it is connected right + // now (direct vs relay); others show their address. + const statusText = isConnectingRow + ? t('mobile.connect.connecting') + : isActive + ? (isRelayModeActive() ? t('mobile.instances.status.connectedRelay') : t('mobile.instances.status.connectedDirect')) + : connection.candidates.some((c) => c.kind === 'direct') ? connectionDisplayUrl(connection) : t('mobile.connect.relay.badge'); return (
void connect({ id: connection.id, candidates: connection.candidates, clientToken: connection.clientToken, label: connection.label })} - disabled={isBusy || confirming} + onClick={() => { + if (isActive) return; + setConnectingId(connection.id); + void connect({ id: connection.id, candidates: connection.candidates, clientToken: connection.clientToken, label: connection.label }) + .finally(() => setConnectingId(null)); + }} + disabled={(isBusy && !isConnectingRow) || confirming} > - + + {isActive ? ( + + ) : null} {connection.label} - - {connection.candidates.some((c) => c.kind === 'direct') ? connectionDisplayUrl(connection) : t('mobile.connect.relay.badge')} + + {statusText} + {isConnectingRow ? : null}
{confirming ? ( @@ -1151,76 +1200,88 @@ const MobileInstancesSurface: React.FC<{

)} -
-
-

- {editingConnection ? t('mobile.instances.editTitle') : t('mobile.instances.addTitle')} -

- {editingConnection ? ( + {/* Add actions: QR pairing is the primary path; the manual form stays + hidden until asked for (or until a row's edit button opens it). */} + {!formOpen && !editingConnection ? ( +
+ {qrScanSupported ? ( + + ) : null} + + {error ?

{error}

: null} +
+ ) : ( + +
+

+ {editingConnection ? t('mobile.instances.editTitle') : t('mobile.instances.addTitle')} +

- ) : null} -
-
-
+ + + + {error ?

{error}

: null} + - {!qrScanSupported ? ( -

{t('mobile.connect.scan.unsupported')}

- ) : null} -
- - - - {error ?

{error}

: null} - -
+ + )}
@@ -2793,8 +2854,14 @@ export function MobileApp({ apis }: MobileAppProps) { }, [setIsMobile]); React.useEffect(() => { + // Never bootstrap without a runtime endpoint on native: with apiBaseUrl '' + // the resolver falls back to the webview's own origin, where Capacitor's + // static server answers every request with index.html — the bootstrap + // "succeeds" against a fake backend and flips isConnected back on, leaving + // the user in an empty shell after a disconnect. + if (isNativeMobileApp && !getRuntimeApiBaseUrl()) return; void initializeApp(); - }, [connectionEpoch, initializeApp]); + }, [connectionEpoch, initializeApp, isNativeMobileApp]); React.useEffect(() => { if (!isConnected) return; @@ -2937,10 +3004,16 @@ export function MobileApp({ apis }: MobileAppProps) { ); } - if (!isConnected && !isReconnecting && isNativeMobileApp) { + // No runtime endpoint on native = explicitly disconnected (last instance + // deleted, revoked token, unreachable). The connect screen is the only valid + // UI then — regardless of what a stale isConnected flag claims (the store can + // be poisoned by a bootstrap that ran against the webview's own origin). + const hasRuntimeEndpoint = Boolean(getRuntimeApiBaseUrl()); + + if (isNativeMobileApp && (!hasRuntimeEndpoint || (!isConnected && !isReconnecting))) { // A runtime endpoint is already selected (first connect or switching instances): // show a loader while it re-bootstraps instead of flashing the onboarding screen. - if (getRuntimeApiBaseUrl()) { + if (hasRuntimeEndpoint) { return (
diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 18add70e..a0896c0b 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -51,7 +51,9 @@ export const dict = { 'mobile.connect.cancelPassword': 'Use another server', 'mobile.connect.connecting': 'Connecting...', 'mobile.connect.scanQr': 'Scan QR code', + 'mobile.connect.welcome.scanHint': 'On your computer, open «Add a device» to show a QR code, then scan it here.', 'mobile.connect.advanced': 'Advanced', + 'mobile.connect.manual.toggle': 'Connect by address', 'mobile.connect.scan.permissionDenied': 'Camera access is off. Enable it in Settings to scan a QR code.', 'mobile.connect.scan.failed': 'Could not scan that QR code. Try again or enter the URL manually.', 'mobile.connect.scan.invalid': 'That QR code is not an OpenChamber connection code.', @@ -65,6 +67,7 @@ export const dict = { 'mobile.connect.error.authRequired': 'This server needs a password or client token.', 'mobile.connect.error.passwordFailed': 'Could not unlock that server. Check the password.', 'mobile.instances.addTitle': 'Add instance', + 'mobile.instances.addManual': 'Add by address', 'mobile.instances.editTitle': 'Edit instance', 'mobile.instances.edit': 'Edit', 'mobile.instances.delete': 'Delete', @@ -75,6 +78,8 @@ export const dict = { 'mobile.instances.label.label': 'Name', 'mobile.instances.label.placeholder': 'Optional display name', 'mobile.instances.saveNew': 'Save instance', + 'mobile.instances.status.connectedDirect': 'Connected · Local network', + 'mobile.instances.status.connectedRelay': 'Connected · Private relay', 'mobile.instances.saveEdit': 'Save changes', 'mobile.nav.changes': 'Changes', 'mobile.nav.settings': 'Settings', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 1920ce1c..55e5be06 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -52,7 +52,9 @@ export const dict: Record = { "mobile.connect.cancelPassword": "Usar otro servidor", "mobile.connect.connecting": "Conectando...", "mobile.connect.scanQr": "Escanear código QR", + "mobile.connect.welcome.scanHint": "En tu ordenador, abre «Añadir un dispositivo» para mostrar un código QR y escanéalo aquí.", "mobile.connect.advanced": "Avanzado", + "mobile.connect.manual.toggle": "Conectar por dirección", "mobile.connect.scan.permissionDenied": "El acceso a la cámara está desactivado. Actívalo en Ajustes para escanear un código QR.", "mobile.connect.scan.failed": "No se pudo escanear ese código QR. Inténtalo de nuevo o introduce la URL manualmente.", "mobile.connect.scan.invalid": "Ese código QR no es un código de conexión de OpenChamber.", @@ -66,6 +68,7 @@ export const dict: Record = { "mobile.connect.error.authRequired": "Este servidor requiere una contraseña o un token de cliente.", "mobile.connect.error.passwordFailed": "No se pudo desbloquear ese servidor. Revisa la contraseña.", "mobile.instances.addTitle": "Agregar instancia", + "mobile.instances.addManual": "Añadir por dirección", "mobile.instances.editTitle": "Editar instancia", "mobile.instances.edit": "Editar", "mobile.instances.delete": "Eliminar", @@ -76,6 +79,8 @@ export const dict: Record = { "mobile.instances.label.label": "Nombre", "mobile.instances.label.placeholder": "Nombre para mostrar (opcional)", "mobile.instances.saveNew": "Guardar instancia", + "mobile.instances.status.connectedDirect": "Conectado · Red local", + "mobile.instances.status.connectedRelay": "Conectado · Relay privado", "mobile.instances.saveEdit": "Guardar cambios", "mobile.nav.changes": "Cambios", "mobile.nav.settings": "Ajustes", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 675f5ecc..8af49a6f 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -2509,7 +2509,9 @@ export const dict = { 'mobile.connect.cancelPassword': 'Utiliser un autre serveur', 'mobile.connect.connecting': 'Connexion...', 'mobile.connect.scanQr': 'Scanner le code QR', + 'mobile.connect.welcome.scanHint': 'Sur votre ordinateur, ouvrez « Ajouter un appareil » pour afficher un code QR, puis scannez-le ici.', 'mobile.connect.advanced': 'Avancé', + 'mobile.connect.manual.toggle': 'Connexion par adresse', 'mobile.connect.scan.permissionDenied': 'L\'accès à la caméra est désactivé. Activez-le dans les Réglages pour scanner un code QR.', 'mobile.connect.scan.failed': 'Impossible de scanner ce code QR. Réessayez ou saisissez l\'URL manuellement.', 'mobile.connect.scan.invalid': 'Ce code QR n\'est pas un code de connexion OpenChamber.', @@ -2523,6 +2525,7 @@ export const dict = { 'mobile.connect.error.authRequired': 'Ce serveur nécessite un mot de passe ou un jeton client.', 'mobile.connect.error.passwordFailed': 'Impossible de déverrouiller ce serveur. Vérifiez le mot de passe.', 'mobile.instances.addTitle': 'Ajouter une instance', + 'mobile.instances.addManual': 'Ajouter par adresse', 'mobile.instances.editTitle': 'Modifier l\'instance', 'mobile.instances.edit': 'Modifier', 'mobile.instances.delete': 'Supprimer', @@ -2533,6 +2536,8 @@ export const dict = { 'mobile.instances.label.label': 'Nom', 'mobile.instances.label.placeholder': 'Nom d\'affichage facultatif', 'mobile.instances.saveNew': 'Enregistrer l\'instance', + 'mobile.instances.status.connectedDirect': 'Connecté · Réseau local', + 'mobile.instances.status.connectedRelay': 'Connecté · Relais privé', 'mobile.instances.saveEdit': 'Enregistrer les modifications', 'mobile.nav.changes': 'Modifications', 'mobile.nav.settings': 'Paramètres', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index 8e7ba90a..82b6c394 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -46,7 +46,9 @@ export const dict: Record = { 'mobile.connect.url.label': 'サーバー URL', 'mobile.connect.url.placeholder': 'http://192.168.1.74:2606', 'mobile.connect.scanQr': 'QR コードをスキャン', + 'mobile.connect.welcome.scanHint': 'コンピュータで「デバイスを追加」を開いてQRコードを表示し、ここでスキャンしてください。', 'mobile.connect.advanced': '詳細設定', + 'mobile.connect.manual.toggle': 'アドレスで接続', 'mobile.connect.token.label': 'クライアントトークン', 'mobile.connect.token.placeholder': 'アクセストークンを貼り付け', 'mobile.connect.token.hint': 'サーバーがパスワードの代わりにトークンを必要とする場合のみ必要です。', @@ -69,10 +71,13 @@ export const dict: Record = { 'mobile.connect.scan.invalid': 'その QR コードは OpenChamber の接続コードではありません。', 'mobile.connect.scan.failed': 'その QR コードを読み取れませんでした。もう一度試すか、URL を手動で入力してください。', 'mobile.instances.addTitle': 'インスタンスを追加', + 'mobile.instances.addManual': 'アドレスで追加', 'mobile.instances.editTitle': 'インスタンスを編集', 'mobile.instances.label.label': '名前', 'mobile.instances.label.placeholder': '表示名(任意)', 'mobile.instances.saveNew': 'インスタンスを保存', + 'mobile.instances.status.connectedDirect': '接続中 · ローカルネットワーク', + 'mobile.instances.status.connectedRelay': '接続中 · プライベートリレー', 'mobile.instances.saveEdit': '変更を保存', 'mobile.instances.cancelEdit': 'キャンセル', 'mobile.instances.edit': '編集', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index acfa1e66..1f3ee29a 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -52,7 +52,9 @@ export const dict: Record = { 'mobile.connect.cancelPassword': '다른 서버 사용', 'mobile.connect.connecting': '연결 중...', 'mobile.connect.scanQr': 'QR 코드 스캔', + 'mobile.connect.welcome.scanHint': '컴퓨터에서 「기기 추가」를 열어 QR 코드를 표시한 뒤 여기에서 스캔하세요.', 'mobile.connect.advanced': '고급', + 'mobile.connect.manual.toggle': '주소로 연결', 'mobile.connect.scan.permissionDenied': '카메라 접근이 꺼져 있습니다. QR 코드를 스캔하려면 설정에서 사용 설정하세요.', 'mobile.connect.scan.failed': 'QR 코드를 스캔하지 못했습니다. 다시 시도하거나 URL을 직접 입력하세요.', 'mobile.connect.scan.invalid': '이 QR 코드는 OpenChamber 연결 코드가 아닙니다.', @@ -66,6 +68,7 @@ export const dict: Record = { 'mobile.connect.error.authRequired': '이 서버에는 비밀번호 또는 클라이언트 토큰이 필요합니다.', 'mobile.connect.error.passwordFailed': '서버 잠금을 해제할 수 없습니다. 비밀번호를 확인하세요.', 'mobile.instances.addTitle': '인스턴스 추가', + 'mobile.instances.addManual': '주소로 추가', 'mobile.instances.editTitle': '인스턴스 편집', 'mobile.instances.edit': '편집', 'mobile.instances.delete': '삭제', @@ -76,6 +79,8 @@ export const dict: Record = { 'mobile.instances.label.label': '이름', 'mobile.instances.label.placeholder': '표시 이름 (선택 사항)', 'mobile.instances.saveNew': '인스턴스 저장', + 'mobile.instances.status.connectedDirect': '연결됨 · 로컬 네트워크', + 'mobile.instances.status.connectedRelay': '연결됨 · 비공개 릴레이', 'mobile.instances.saveEdit': '변경 사항 저장', 'mobile.nav.changes': '변경사항', 'mobile.nav.settings': '설정', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index fba65b82..5693f210 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -53,7 +53,9 @@ export const dict: Record = { 'mobile.connect.cancelPassword': 'Użyj innego serwera', 'mobile.connect.connecting': 'Łączenie...', 'mobile.connect.scanQr': 'Skanuj kod QR', + 'mobile.connect.welcome.scanHint': 'Na komputerze otwórz «Dodaj urządzenie», aby wyświetlić kod QR, i zeskanuj go tutaj.', 'mobile.connect.advanced': 'Zaawansowane', + 'mobile.connect.manual.toggle': 'Połącz przez adres', 'mobile.connect.scan.permissionDenied': 'Dostęp do aparatu jest wyłączony. Włącz go w Ustawieniach, aby zeskanować kod QR.', 'mobile.connect.scan.failed': 'Nie udało się zeskanować tego kodu QR. Spróbuj ponownie lub wpisz adres URL ręcznie.', 'mobile.connect.scan.invalid': 'Ten kod QR nie jest kodem połączenia OpenChamber.', @@ -67,6 +69,7 @@ export const dict: Record = { 'mobile.connect.error.authRequired': 'Ten serwer wymaga hasła lub tokenu klienta.', 'mobile.connect.error.passwordFailed': 'Nie udało się odblokować tego serwera. Sprawdź hasło.', 'mobile.instances.addTitle': 'Dodaj instancję', + 'mobile.instances.addManual': 'Dodaj przez adres', 'mobile.instances.editTitle': 'Edytuj instancję', 'mobile.instances.edit': 'Edytuj', 'mobile.instances.delete': 'Usuń', @@ -77,6 +80,8 @@ export const dict: Record = { 'mobile.instances.label.label': 'Nazwa', 'mobile.instances.label.placeholder': 'Opcjonalna nazwa wyświetlana', 'mobile.instances.saveNew': 'Zapisz instancję', + 'mobile.instances.status.connectedDirect': 'Połączono · Sieć lokalna', + 'mobile.instances.status.connectedRelay': 'Połączono · Prywatny relay', 'mobile.instances.saveEdit': 'Zapisz zmiany', 'mobile.nav.changes': 'Zmiany', 'mobile.nav.settings': 'Ustawienia', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 7d00fcdf..f30fac68 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -52,7 +52,9 @@ export const dict: Record = { "mobile.connect.cancelPassword": "Usar outro servidor", "mobile.connect.connecting": "Conectando...", "mobile.connect.scanQr": "Ler código QR", + "mobile.connect.welcome.scanHint": "No seu computador, abra «Adicionar um dispositivo» para mostrar um código QR e escaneie aqui.", "mobile.connect.advanced": "Avançado", + "mobile.connect.manual.toggle": "Conectar por endereço", "mobile.connect.scan.permissionDenied": "O acesso à câmera está desativado. Ative-o nos Ajustes para ler um código QR.", "mobile.connect.scan.failed": "Não foi possível ler esse código QR. Tente novamente ou digite a URL manualmente.", "mobile.connect.scan.invalid": "Esse código QR não é um código de conexão do OpenChamber.", @@ -66,6 +68,7 @@ export const dict: Record = { "mobile.connect.error.authRequired": "Este servidor requer uma senha ou token do cliente.", "mobile.connect.error.passwordFailed": "Não foi possível desbloquear esse servidor. Verifique a senha.", "mobile.instances.addTitle": "Adicionar instância", + "mobile.instances.addManual": "Adicionar por endereço", "mobile.instances.editTitle": "Editar instância", "mobile.instances.edit": "Editar", "mobile.instances.delete": "Excluir", @@ -76,6 +79,8 @@ export const dict: Record = { "mobile.instances.label.label": "Nome", "mobile.instances.label.placeholder": "Nome de exibição opcional", "mobile.instances.saveNew": "Salvar instância", + "mobile.instances.status.connectedDirect": "Conectado · Rede local", + "mobile.instances.status.connectedRelay": "Conectado · Relay privado", "mobile.instances.saveEdit": "Salvar alterações", "mobile.nav.changes": "Alterações", "mobile.nav.settings": "Configurações", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 01b822f3..522eb0b2 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -52,7 +52,9 @@ export const dict: Record = { "mobile.connect.cancelPassword": "Інший сервер", "mobile.connect.connecting": "Підключення...", "mobile.connect.scanQr": "Сканувати QR-код", + "mobile.connect.welcome.scanHint": "На компʼютері відкрийте «Додати пристрій», щоб показати QR-код, і відскануйте його тут.", "mobile.connect.advanced": "Додатково", + "mobile.connect.manual.toggle": "Підключитися за адресою", "mobile.connect.scan.permissionDenied": "Доступ до камери вимкнено. Увімкни його в Налаштуваннях, щоб сканувати QR-код.", "mobile.connect.scan.failed": "Не вдалося відсканувати QR-код. Спробуй ще раз або введи адресу вручну.", "mobile.connect.scan.invalid": "Це не QR-код підключення OpenChamber.", @@ -66,6 +68,7 @@ export const dict: Record = { "mobile.connect.error.authRequired": "Цьому серверу потрібен пароль або client token.", "mobile.connect.error.passwordFailed": "Не вдалося розблокувати сервер. Перевір пароль.", "mobile.instances.addTitle": "Додати інстанс", + "mobile.instances.addManual": "Додати за адресою", "mobile.instances.editTitle": "Редагувати інстанс", "mobile.instances.edit": "Редагувати", "mobile.instances.delete": "Видалити", @@ -76,6 +79,8 @@ export const dict: Record = { "mobile.instances.label.label": "Назва", "mobile.instances.label.placeholder": "Необовʼязкова назва", "mobile.instances.saveNew": "Зберегти інстанс", + "mobile.instances.status.connectedDirect": "Підключено · Локальна мережа", + "mobile.instances.status.connectedRelay": "Підключено · Приватний relay", "mobile.instances.saveEdit": "Зберегти зміни", "mobile.nav.changes": "Зміни", "mobile.nav.settings": "Налаштування", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 4c558413..0381efa0 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -52,7 +52,9 @@ export const dict: Record = { 'mobile.connect.cancelPassword': '使用其他服务器', 'mobile.connect.connecting': '连接中...', 'mobile.connect.scanQr': '扫描二维码', + 'mobile.connect.welcome.scanHint': '在电脑上打开「添加设备」显示二维码,然后在这里扫描。', 'mobile.connect.advanced': '高级', + 'mobile.connect.manual.toggle': '通过地址连接', 'mobile.connect.scan.permissionDenied': '相机访问已关闭。请在“设置”中开启以扫描二维码。', 'mobile.connect.scan.failed': '无法扫描该二维码。请重试或手动输入网址。', 'mobile.connect.scan.invalid': '该二维码不是 OpenChamber 连接码。', @@ -66,6 +68,7 @@ export const dict: Record = { 'mobile.connect.error.authRequired': '该服务器需要密码或客户端令牌。', 'mobile.connect.error.passwordFailed': '无法解锁该服务器。请检查密码。', 'mobile.instances.addTitle': '添加实例', + 'mobile.instances.addManual': '通过地址添加', 'mobile.instances.editTitle': '编辑实例', 'mobile.instances.edit': '编辑', 'mobile.instances.delete': '删除', @@ -76,6 +79,8 @@ export const dict: Record = { 'mobile.instances.label.label': '名称', 'mobile.instances.label.placeholder': '可选显示名称', 'mobile.instances.saveNew': '保存实例', + 'mobile.instances.status.connectedDirect': '已连接 · 局域网', + 'mobile.instances.status.connectedRelay': '已连接 · 私有中继', 'mobile.instances.saveEdit': '保存更改', 'mobile.nav.changes': '更改', 'mobile.nav.settings': '设置', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index c24f8d36..909a673a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -52,7 +52,9 @@ export const dict: Record = { 'mobile.connect.cancelPassword': '使用其他伺服器', 'mobile.connect.connecting': '連線中...', 'mobile.connect.scanQr': '掃描 QR code', + 'mobile.connect.welcome.scanHint': '在電腦上開啟「新增裝置」顯示 QR 代碼,然後在這裡掃描。', 'mobile.connect.advanced': '進階', + 'mobile.connect.manual.toggle': '透過位址連線', 'mobile.connect.scan.permissionDenied': '相機存取已關閉。請在「設定」中開啟以掃描 QR code。', 'mobile.connect.scan.failed': '無法掃描該 QR code。請重試或手動輸入網址。', 'mobile.connect.scan.invalid': '此 QR code 不是 OpenChamber 連線代碼。', @@ -66,6 +68,7 @@ export const dict: Record = { 'mobile.connect.error.authRequired': '此伺服器需要密碼或用戶端權杖。', 'mobile.connect.error.passwordFailed': '無法解鎖該伺服器。請檢查密碼。', 'mobile.instances.addTitle': '新增執行個體', + 'mobile.instances.addManual': '透過位址新增', 'mobile.instances.editTitle': '編輯執行個體', 'mobile.instances.edit': '編輯', 'mobile.instances.delete': '刪除', @@ -76,6 +79,8 @@ export const dict: Record = { 'mobile.instances.label.label': '名稱', 'mobile.instances.label.placeholder': '選填顯示名稱', 'mobile.instances.saveNew': '儲存執行個體', + 'mobile.instances.status.connectedDirect': '已連線 · 區域網路', + 'mobile.instances.status.connectedRelay': '已連線 · 私人中繼', 'mobile.instances.saveEdit': '儲存變更', 'mobile.nav.changes': '變更', 'mobile.nav.settings': '設定',