feat: add DNS management to Network settings

- Add DNS configuration APIs for manual and DHCP modes, including persistence and udhcpc hook support.
- Add a Network settings tab with DNS controls, move Wi-Fi and HTTPS settings into it, and update translations.
- Update locales
This commit is contained in:
wenjie
2026-05-07 17:03:25 +08:00
parent 1c43542919
commit 36cf9c30d8
36 changed files with 2310 additions and 232 deletions

View File

@@ -1,5 +1,7 @@
import { http } from '@/lib/http.ts';
export type DNSMode = 'manual' | 'dhcp';
// wake on lan
export function wol(mac: string) {
const data = {
@@ -69,3 +71,11 @@ export function connectWifi(ssid: string, password: string) {
export function disconnectWifi() {
return http.post('/api/network/wifi/disconnect');
}
export function getDNS() {
return http.get('/api/network/dns');
}
export function setDNS(mode: DNSMode, servers: string[]) {
return http.post('/api/network/dns', { mode, servers });
}

View File

@@ -4,9 +4,12 @@ import clsx from 'clsx';
function ScrollArea({
className,
viewportRef,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {
viewportRef?: React.Ref<HTMLDivElement>;
}) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
@@ -14,6 +17,7 @@ function ScrollArea({
{...props}
>
<ScrollAreaPrimitive.Viewport
ref={viewportRef}
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px]"
>

View File

@@ -226,19 +226,10 @@ const ca = {
1800: "30 min",
3600: "1 h"
},
wifi: {
title: "Xarxa sense fils",
description: "Configura la xarxa sense fils",
setBtn: "Configura"
},
ssh: {
description: "Activa accés remot per SSH",
tip: "Configura una contrasenya segura abans (Compte - Canvia contrasenya)"
},
tls: {
description: "Activa el protocol HTTPS",
tip: "Atenció: Usar HTTPS pot augmentar la latència, sobretot amb vídeo MJPEG."
},
advanced: "Configuració avançada",
swap: {
title: "Swap",
@@ -270,6 +261,52 @@ const ca = {
okBtn: "Sí",
cancelBtn: "No"
},
network: {
title: 'Xarxa',
wifi: {
title: 'Wi-Fi',
description: 'Configura la xarxa Wi-Fi',
apMode: 'El mode AP està activat; connecta\'t al Wi-Fi escanejant el codi QR',
connect: 'Connecta\'t a Wi-Fi',
connectDesc1: 'Introdueix el SSID i la contrasenya de la xarxa',
connectDesc2: 'Introdueix la contrasenya per connectar-te a aquesta xarxa',
disconnect: 'Segur que vols desconnectar la xarxa?',
failed: 'La connexió ha fallat, torna-ho a provar.',
ssid: 'Nom',
password: 'Contrasenya',
joinBtn: 'Connecta',
confirmBtn: 'D\'acord',
cancelBtn: 'Cancel·la'
},
tls: {
description: 'Activa el protocol HTTPS',
tip: 'Atenció: Usar HTTPS pot augmentar la latència, sobretot amb vídeo MJPEG.'
},
dns: {
title: 'DNS',
description: 'Configura els servidors DNS per a NanoKVM',
mode: 'Mode',
dhcp: 'DHCP',
manual: 'Manual',
add: 'Afegeix DNS',
save: 'Desa',
invalid: 'Introdueix una adreça IP vàlida',
noDhcp: 'No hi ha cap DNS DHCP disponible actualment',
saved: 'Configuració DNS desada',
saveFailed: 'No s\'ha pogut desar la configuració DNS',
unsaved: 'Canvis no desats',
maxServers: 'Es permeten com a màxim {{count}} servidors DNS',
dnsServers: 'Servidors DNS',
dhcpServersDescription: 'Els servidors DNS s\'obtenen automàticament via DHCP.',
manualServersDescription: 'Els servidors DNS es poden editar manualment.',
networkDetails: 'Detalls de xarxa',
interface: 'Interfície',
ipAddress: 'Adreça IP',
subnetMask: 'Màscara de subxarxa',
router: 'Encaminador',
none: 'Cap'
}
},
tailscale: {
title: "Tailscale",
memory: {

View File

@@ -183,16 +183,57 @@ const cz = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Síť',
wifi: {
title: 'Wi-Fi',
description: 'Nastavit Wi-Fi',
apMode: 'Režim AP je povolen, připojte se k Wi-Fi naskenováním QR kódu',
connect: 'Připojit Wi-Fi',
connectDesc1: 'Zadejte SSID sítě a heslo',
connectDesc2: 'Zadejte heslo pro připojení k této síti',
disconnect: 'Opravdu chcete síť odpojit?',
failed: 'Připojení se nezdařilo, zkuste to znovu.',
ssid: 'Název',
password: 'Heslo',
joinBtn: 'Připojit',
confirmBtn: 'OK',
cancelBtn: 'Zrušit'
},
tls: {
description: 'Povolit protokol HTTPS',
tip: 'Upozornění: Použití HTTPS může zvýšit latenci, zejména v režimu videa MJPEG.'
},
dns: {
title: 'DNS',
description: 'Nastavit DNS servery pro NanoKVM',
mode: 'Režim',
dhcp: 'DHCP',
manual: 'Ručně',
add: 'Přidat DNS',
save: 'Uložit',
invalid: 'Zadejte platnou IP adresu',
noDhcp: 'Momentálně není k dispozici žádné DHCP DNS',
saved: 'Nastavení DNS uloženo',
saveFailed: 'Nastavení DNS se nepodařilo uložit',
unsaved: 'Neuložené změny',
maxServers: 'Je povoleno maximálně {{count}} DNS serverů',
dnsServers: 'DNS servery',
dhcpServersDescription: 'DNS servery jsou automaticky získávány z DHCP.',
manualServersDescription: 'DNS servery lze upravit ručně.',
networkDetails: 'Podrobnosti sítě',
interface: 'Rozhraní',
ipAddress: 'IP adresa',
subnetMask: 'Maska podsítě',
router: 'Router',
none: 'Žádné'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -181,16 +181,57 @@ const da = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Netværk',
wifi: {
title: 'Wi-Fi',
description: 'Konfigurer Wi-Fi',
apMode: 'AP-tilstand er aktiveret, opret forbindelse til Wi-Fi ved at scanne QR-koden',
connect: 'Tilslut Wi-Fi',
connectDesc1: 'Indtast netværkets SSID og adgangskode',
connectDesc2: 'Indtast adgangskoden for at tilslutte dette netværk',
disconnect: 'Er du sikker på, at du vil afbryde netværket?',
failed: 'Forbindelsen mislykkedes, prøv igen.',
ssid: 'Navn',
password: 'Adgangskode',
joinBtn: 'Tilslut',
confirmBtn: 'OK',
cancelBtn: 'Annuller'
},
tls: {
description: 'Aktiver HTTPS-protokol',
tip: 'Bemærk: Brug af HTTPS kan øge forsinkelsen, især med MJPEG-videotilstand.'
},
dns: {
title: 'DNS',
description: 'Konfigurer DNS-servere til NanoKVM',
mode: 'Tilstand',
dhcp: 'DHCP',
manual: 'Manuel',
add: 'Tilføj DNS',
save: 'Gem',
invalid: 'Indtast en gyldig IP-adresse',
noDhcp: 'Ingen DHCP-DNS er tilgængelig i øjeblikket',
saved: 'DNS-indstillinger gemt',
saveFailed: 'DNS-indstillinger kunne ikke gemmes',
unsaved: 'Ikke-gemte ændringer',
maxServers: 'Maksimalt {{count}} DNS-servere er tilladt',
dnsServers: 'DNS-servere',
dhcpServersDescription: 'DNS-servere hentes automatisk fra DHCP.',
manualServersDescription: 'DNS-servere kan redigeres manuelt.',
networkDetails: 'Netværksdetaljer',
interface: 'Grænseflade',
ipAddress: 'IP-adresse',
subnetMask: 'Undernetmaske',
router: 'Router',
none: 'Ingen'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -238,11 +238,6 @@ const de = {
1800: '30 Min',
3600: '1 Stunde'
},
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi konfigurieren',
setBtn: 'Konfigurieren'
},
ssh: {
description: 'Aktiviere entfernten SSH-Zugang',
tip: 'Setzten Sie ein starkes Passwort vor dem aktivieren (Konto - Passwort ändern)'
@@ -275,6 +270,52 @@ const de = {
okBtn: 'Ja',
cancelBtn: 'Nein'
},
network: {
title: 'Netzwerk',
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi konfigurieren',
apMode: 'AP-Modus ist aktiviert, verbinden Sie sich per QR-Code mit dem Wi-Fi',
connect: 'Wi-Fi verbinden',
connectDesc1: 'Bitte geben Sie die Netzwerk-SSID und das Passwort ein',
connectDesc2: 'Bitte geben Sie das Passwort ein, um diesem Netzwerk beizutreten',
disconnect: 'Möchten Sie die Netzwerkverbindung wirklich trennen?',
failed: 'Verbindung fehlgeschlagen, bitte erneut versuchen.',
ssid: 'Name',
password: 'Passwort',
joinBtn: 'Verbinden',
confirmBtn: 'OK',
cancelBtn: 'Abbrechen'
},
tls: {
description: 'HTTPS-Protokoll aktivieren',
tip: 'Hinweis: Die Verwendung von HTTPS kann die Latenz erhöhen, besonders im MJPEG-Videomodus.'
},
dns: {
title: 'DNS',
description: 'DNS-Server für NanoKVM konfigurieren',
mode: 'Modus',
dhcp: 'DHCP',
manual: 'Manuell',
add: 'DNS hinzufügen',
save: 'Speichern',
invalid: 'Bitte geben Sie eine gültige IP-Adresse ein',
noDhcp: 'Derzeit ist kein DHCP-DNS verfügbar',
saved: 'DNS-Einstellungen gespeichert',
saveFailed: 'DNS-Einstellungen konnten nicht gespeichert werden',
unsaved: 'Ungespeicherte Änderungen',
maxServers: 'Maximal {{count}} DNS-Server erlaubt',
dnsServers: 'DNS-Server',
dhcpServersDescription: 'DNS-Server werden automatisch per DHCP bezogen.',
manualServersDescription: 'DNS-Server können manuell bearbeitet werden.',
networkDetails: 'Netzwerkdetails',
interface: 'Schnittstelle',
ipAddress: 'IP-Adresse',
subnetMask: 'Subnetzmaske',
router: 'Router',
none: 'Keine'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -309,29 +309,10 @@ const en = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
apMode: 'AP mode is enabled, connect to Wi-Fi by scanning QR code',
connect: 'Join Wi-Fi',
connectDesc1: 'Please enter the network ssid and password',
connectDesc2: 'Please enter the password to join this network',
disconnect: 'Are you sure to disconnect the network?',
failed: 'Connection failed, please try again.',
ssid: 'Name',
password: 'Password',
joinBtn: 'Join',
confirmBtn: 'Ok',
cancelBtn: 'Cancel'
},
ssh: {
description: 'Enable SSH remote access',
tip: 'Set a strong password before enabling (Account - Change Password)'
},
tls: {
description: 'Enable HTTPS protocol',
tip: 'Be aware: Using HTTPS can increase latency, especially with MJPEG video mode.'
},
advanced: 'Advanced Settings',
swap: {
title: 'Swap',
@@ -375,6 +356,52 @@ const en = {
okBtn: 'Yes',
cancelBtn: 'No'
},
network: {
title: 'Network',
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
apMode: 'AP mode is enabled, connect to Wi-Fi by scanning QR code',
connect: 'Join Wi-Fi',
connectDesc1: 'Please enter the network ssid and password',
connectDesc2: 'Please enter the password to join this network',
disconnect: 'Are you sure to disconnect the network?',
failed: 'Connection failed, please try again.',
ssid: 'Name',
password: 'Password',
joinBtn: 'Join',
confirmBtn: 'Ok',
cancelBtn: 'Cancel'
},
tls: {
description: 'Enable HTTPS protocol',
tip: 'Be aware: Using HTTPS can increase latency, especially with MJPEG video mode.'
},
dns: {
title: 'DNS',
description: 'Configure DNS servers for NanoKVM',
mode: 'Mode',
dhcp: 'DHCP',
manual: 'Manual',
add: 'Add DNS',
save: 'Save',
invalid: 'Please enter a valid IP address',
noDhcp: 'No DHCP DNS is currently available',
saved: 'DNS settings saved',
saveFailed: 'Failed to save DNS settings',
unsaved: 'Unsaved changes',
maxServers: 'Maximum {{count}} DNS servers allowed',
dnsServers: 'DNS Servers',
dhcpServersDescription: 'DNS servers are automatically obtained from DHCP.',
manualServersDescription: 'DNS servers can be edited manually.',
networkDetails: 'Network Details',
interface: 'Interface',
ipAddress: 'IP Address',
subnetMask: 'Subnet Mask',
router: 'Router',
none: 'None'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -237,19 +237,10 @@ const es = {
1800: '30 min',
3600: '1 hora'
},
wifi: {
title: 'Wi-Fi',
description: 'Configura el Wi-Fi',
setBtn: 'Configurar'
},
ssh: {
description: 'Habilitar acceso remoto SSH',
tip: 'Establece una contraseña segura antes de habilitar (Cuenta - Cambiar contraseña)'
},
tls: {
description: 'Habilitar protocolo HTTPS',
tip: 'Aviso: Usar HTTPS puede aumentar la latencia, especialmente en modo de vídeo MJPEG.'
},
advanced: 'Ajustes avanzados',
swap: {
title: 'Memoria Swap',
@@ -285,6 +276,52 @@ const es = {
okBtn: 'Sí',
cancelBtn: 'No'
},
network: {
title: 'Red',
wifi: {
title: 'Wi-Fi',
description: 'Configura el Wi-Fi',
apMode: 'El modo AP está activado; conéctate al Wi-Fi escaneando el código QR',
connect: 'Conectar Wi-Fi',
connectDesc1: 'Introduce el SSID de la red y la contraseña',
connectDesc2: 'Introduce la contraseña para unirte a esta red',
disconnect: '¿Seguro que quieres desconectar la red?',
failed: 'Error de conexión, inténtalo de nuevo.',
ssid: 'Nombre',
password: 'Contraseña',
joinBtn: 'Unirse',
confirmBtn: 'Aceptar',
cancelBtn: 'Cancelar'
},
tls: {
description: 'Habilitar protocolo HTTPS',
tip: 'Aviso: Usar HTTPS puede aumentar la latencia, especialmente en modo de vídeo MJPEG.'
},
dns: {
title: 'DNS',
description: 'Configura los servidores DNS para NanoKVM',
mode: 'Modo',
dhcp: 'DHCP',
manual: 'Manual',
add: 'Añadir DNS',
save: 'Guardar',
invalid: 'Introduce una dirección IP válida',
noDhcp: 'No hay DNS DHCP disponible actualmente',
saved: 'Configuración DNS guardada',
saveFailed: 'No se pudo guardar la configuración DNS',
unsaved: 'Cambios sin guardar',
maxServers: 'Se permiten como máximo {{count}} servidores DNS',
dnsServers: 'Servidores DNS',
dhcpServersDescription: 'Los servidores DNS se obtienen automáticamente por DHCP.',
manualServersDescription: 'Los servidores DNS se pueden editar manualmente.',
networkDetails: 'Detalles de red',
interface: 'Interfaz',
ipAddress: 'Dirección IP',
subnetMask: 'Máscara de subred',
router: 'Router',
none: 'Ninguno'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -194,16 +194,57 @@ const fr = {
1800: '30 min',
3600: '1 heure'
},
wifi: {
title: 'Wi-Fi',
description: 'Configurez le Wi-Fi',
setBtn: 'Configurer'
},
disk: 'Disque virtuel',
diskDesc: 'Monter le disque virtuel U sur l\'hôte distant',
network: 'Réseau virtuel',
networkDesc: 'Monter la carte réseau virtuelle sur l\'hôte distant'
},
network: {
title: 'Réseau',
wifi: {
title: 'Wi-Fi',
description: 'Configurez le Wi-Fi',
apMode: 'Le mode AP est activé, connectez-vous au Wi-Fi en scannant le code QR',
connect: 'Connecter le Wi-Fi',
connectDesc1: 'Veuillez saisir le SSID du réseau et le mot de passe',
connectDesc2: 'Veuillez saisir le mot de passe pour rejoindre ce réseau',
disconnect: 'Voulez-vous vraiment déconnecter le réseau ?',
failed: 'Échec de la connexion, veuillez réessayer.',
ssid: 'Nom',
password: 'Mot de passe',
joinBtn: 'Rejoindre',
confirmBtn: 'OK',
cancelBtn: 'Annuler'
},
tls: {
description: 'Activer le protocole HTTPS',
tip: 'Attention : l\'utilisation de HTTPS peut augmenter la latence, surtout en mode vidéo MJPEG.'
},
dns: {
title: 'DNS',
description: 'Configurer les serveurs DNS pour NanoKVM',
mode: 'Mode',
dhcp: 'DHCP',
manual: 'Manuel',
add: 'Ajouter un DNS',
save: 'Enregistrer',
invalid: 'Veuillez saisir une adresse IP valide',
noDhcp: 'Aucun DNS DHCP n\'est actuellement disponible',
saved: 'Paramètres DNS enregistrés',
saveFailed: 'Échec de l\'enregistrement des paramètres DNS',
unsaved: 'Modifications non enregistrées',
maxServers: '{{count}} serveurs DNS maximum autorisés',
dnsServers: 'Serveurs DNS',
dhcpServersDescription: 'Les serveurs DNS sont obtenus automatiquement par DHCP.',
manualServersDescription: 'Les serveurs DNS peuvent être modifiés manuellement.',
networkDetails: 'Détails du réseau',
interface: 'Interface',
ipAddress: 'Adresse IP',
subnetMask: 'Masque de sous-réseau',
router: 'Routeur',
none: 'Aucun'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -183,16 +183,57 @@ const hu = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Hálózat',
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi beállítása',
apMode: 'Az AP mód engedélyezve van, csatlakozzon a Wi-Fihez a QR-kód beolvasásával',
connect: 'Wi-Fi csatlakoztatása',
connectDesc1: 'Adja meg a hálózat SSID-jét és jelszavát',
connectDesc2: 'Adja meg a jelszót a hálózathoz való csatlakozáshoz',
disconnect: 'Biztosan bontja a hálózati kapcsolatot?',
failed: 'A csatlakozás sikertelen, próbálja újra.',
ssid: 'Név',
password: 'Jelszó',
joinBtn: 'Csatlakozás',
confirmBtn: 'OK',
cancelBtn: 'Mégse'
},
tls: {
description: 'HTTPS protokoll engedélyezése',
tip: 'Figyelem: A HTTPS használata növelheti a késleltetést, különösen MJPEG videó módban.'
},
dns: {
title: 'DNS',
description: 'DNS-kiszolgálók beállítása a NanoKVM számára',
mode: 'Mód',
dhcp: 'DHCP',
manual: 'Kézi',
add: 'DNS hozzáadása',
save: 'Mentés',
invalid: 'Adjon meg egy érvényes IP-címet',
noDhcp: 'Jelenleg nincs elérhető DHCP DNS',
saved: 'DNS-beállítások mentve',
saveFailed: 'Nem sikerült menteni a DNS-beállításokat',
unsaved: 'Nem mentett módosítások',
maxServers: 'Legfeljebb {{count}} DNS-kiszolgáló engedélyezett',
dnsServers: 'DNS-kiszolgálók',
dhcpServersDescription: 'A DNS-kiszolgálók automatikusan DHCP-n keresztül érkeznek.',
manualServersDescription: 'A DNS-kiszolgálók kézzel szerkeszthetők.',
networkDetails: 'Hálózati részletek',
interface: 'Interfész',
ipAddress: 'IP-cím',
subnetMask: 'Alhálózati maszk',
router: 'Router',
none: 'Nincs'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -182,16 +182,57 @@ const id = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Jaringan',
wifi: {
title: 'Wi-Fi',
description: 'Konfigurasi Wi-Fi',
apMode: 'Mode AP aktif, sambungkan ke Wi-Fi dengan memindai kode QR',
connect: 'Hubungkan Wi-Fi',
connectDesc1: 'Masukkan SSID jaringan dan kata sandi',
connectDesc2: 'Masukkan kata sandi untuk bergabung ke jaringan ini',
disconnect: 'Yakin ingin memutuskan jaringan?',
failed: 'Koneksi gagal, coba lagi.',
ssid: 'Nama',
password: 'Kata sandi',
joinBtn: 'Gabung',
confirmBtn: 'OK',
cancelBtn: 'Batal'
},
tls: {
description: 'Aktifkan protokol HTTPS',
tip: 'Perhatian: Menggunakan HTTPS dapat meningkatkan latensi, terutama pada mode video MJPEG.'
},
dns: {
title: 'DNS',
description: 'Konfigurasi server DNS untuk NanoKVM',
mode: 'Mode',
dhcp: 'DHCP',
manual: 'Manual',
add: 'Tambah DNS',
save: 'Simpan',
invalid: 'Masukkan alamat IP yang valid',
noDhcp: 'DNS DHCP saat ini tidak tersedia',
saved: 'Pengaturan DNS disimpan',
saveFailed: 'Gagal menyimpan pengaturan DNS',
unsaved: 'Perubahan belum disimpan',
maxServers: 'Maksimal {{count}} server DNS diizinkan',
dnsServers: 'Server DNS',
dhcpServersDescription: 'Server DNS diperoleh otomatis dari DHCP.',
manualServersDescription: 'Server DNS dapat diedit secara manual.',
networkDetails: 'Detail Jaringan',
interface: 'Antarmuka',
ipAddress: 'Alamat IP',
subnetMask: 'Subnet Mask',
router: 'Router',
none: 'Tidak ada'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -183,16 +183,57 @@ const it = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Rete',
wifi: {
title: 'Wi-Fi',
description: 'Configura Wi-Fi',
apMode: 'La modalità AP è attiva, connettiti al Wi-Fi scansionando il codice QR',
connect: 'Connetti Wi-Fi',
connectDesc1: 'Inserisci SSID e password della rete',
connectDesc2: 'Inserisci la password per unirti a questa rete',
disconnect: 'Vuoi davvero disconnettere la rete?',
failed: 'Connessione non riuscita, riprova.',
ssid: 'Nome',
password: 'Password',
joinBtn: 'Connetti',
confirmBtn: 'OK',
cancelBtn: 'Annulla'
},
tls: {
description: 'Abilita protocollo HTTPS',
tip: 'Attenzione: l\'uso di HTTPS può aumentare la latenza, soprattutto in modalità video MJPEG.'
},
dns: {
title: 'DNS',
description: 'Configura i server DNS per NanoKVM',
mode: 'Modalità',
dhcp: 'DHCP',
manual: 'Manuale',
add: 'Aggiungi DNS',
save: 'Salva',
invalid: 'Inserisci un indirizzo IP valido',
noDhcp: 'Nessun DNS DHCP è attualmente disponibile',
saved: 'Impostazioni DNS salvate',
saveFailed: 'Impossibile salvare le impostazioni DNS',
unsaved: 'Modifiche non salvate',
maxServers: 'Sono consentiti al massimo {{count}} server DNS',
dnsServers: 'Server DNS',
dhcpServersDescription: 'I server DNS vengono ottenuti automaticamente da DHCP.',
manualServersDescription: 'I server DNS possono essere modificati manualmente.',
networkDetails: 'Dettagli rete',
interface: 'Interfaccia',
ipAddress: 'Indirizzo IP',
subnetMask: 'Subnet mask',
router: 'Router',
none: 'Nessuno'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -267,29 +267,10 @@ const ja = {
1800: '30分',
3600: '1時間'
},
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi 設定',
apMode: 'AP モードが有効になりました。QR コードをスキャンして Wi-Fi に接続してください。',
connect: 'Wi-Fi に接続',
connectDesc1: 'SSID とパスワードを入力してください',
connectDesc2: 'このネットワークに接続するためのパスワードを入力してください',
disconnect: 'このネットワーク接続を切断しますか?',
failed: '接続に失敗しました。もう一度お試しください。',
ssid: 'SSID',
password: 'パスワード',
joinBtn: '接続',
confirmBtn: 'OK',
cancelBtn: 'キャンセル'
},
ssh: {
description: 'SSH リモートアクセスを有効にする',
tip: '使用する前に必ず強力なパスワードを設定してください(アカウント - パスワードの変更)'
},
tls: {
description: 'HTTPS プロトコルを有効にする',
tip: '注意HTTPS を使用すると、特に MJPEG ビデオモードで遅延が増加する可能性があります。'
},
advanced: '詳細設定',
swap: {
disable: '無効',
@@ -330,6 +311,52 @@ const ja = {
okBtn: 'はい',
cancelBtn: 'いいえ'
},
network: {
title: 'ネットワーク',
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi 設定',
apMode: 'AP モードが有効になりました。QR コードをスキャンして Wi-Fi に接続してください。',
connect: 'Wi-Fi に接続',
connectDesc1: 'SSID とパスワードを入力してください',
connectDesc2: 'このネットワークに接続するためのパスワードを入力してください',
disconnect: 'このネットワーク接続を切断しますか?',
failed: '接続に失敗しました。もう一度お試しください。',
ssid: 'SSID',
password: 'パスワード',
joinBtn: '接続',
confirmBtn: 'OK',
cancelBtn: 'キャンセル'
},
tls: {
description: 'HTTPS プロトコルを有効にする',
tip: '注意HTTPS を使用すると、特に MJPEG ビデオモードで遅延が増加する可能性があります。'
},
dns: {
title: 'DNS',
description: 'NanoKVM の DNS サーバーを設定',
mode: 'モード',
dhcp: 'DHCP',
manual: '手動',
add: 'DNS を追加',
save: '保存',
invalid: '有効な IP アドレスを入力してください',
noDhcp: '現在 DHCP DNS は利用できません',
saved: 'DNS 設定を保存しました',
saveFailed: 'DNS 設定の保存に失敗しました',
unsaved: '未保存の変更',
maxServers: 'DNS サーバーは最大 {{count}} 個までです',
dnsServers: 'DNS サーバー',
dhcpServersDescription: 'DNS サーバーは DHCP から自動取得されます。',
manualServersDescription: 'DNS サーバーは手動で編集できます。',
networkDetails: 'ネットワーク詳細',
interface: 'インターフェイス',
ipAddress: 'IP アドレス',
subnetMask: 'サブネットマスク',
router: 'ルーター',
none: 'なし'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -246,19 +246,10 @@ const ko = {
1800: '30분',
3600: '1시간'
},
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi 설정',
setBtn: '설정'
},
ssh: {
description: 'SSH 원격 접속 활성화',
tip: '활성화하기 전에 강력한 비밀번호를 설정하세요. (계정 - 비밀번호 변경)'
},
tls: {
description: 'HTTPS 프로토콜 활성화',
tip: '주의: HTTPS 사용 시 특히 MJPEG 비디오 모드에서 지연 시간이 증가할 수 있습니다.'
},
advanced: '고급 설정',
swap: {
title: '스왑',
@@ -290,6 +281,52 @@ const ko = {
okBtn: '네',
cancelBtn: '아니오'
},
network: {
title: '네트워크',
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi 설정',
apMode: 'AP 모드가 활성화되었습니다. QR 코드를 스캔하여 Wi-Fi에 연결하세요',
connect: 'Wi-Fi 연결',
connectDesc1: '네트워크 SSID와 비밀번호를 입력하세요',
connectDesc2: '이 네트워크에 연결할 비밀번호를 입력하세요',
disconnect: '네트워크 연결을 끊으시겠습니까?',
failed: '연결에 실패했습니다. 다시 시도하세요.',
ssid: '이름',
password: '비밀번호',
joinBtn: '연결',
confirmBtn: '확인',
cancelBtn: '취소'
},
tls: {
description: 'HTTPS 프로토콜 활성화',
tip: '주의: HTTPS 사용 시 특히 MJPEG 비디오 모드에서 지연 시간이 증가할 수 있습니다.'
},
dns: {
title: 'DNS',
description: 'NanoKVM의 DNS 서버 설정',
mode: '모드',
dhcp: 'DHCP',
manual: '수동',
add: 'DNS 추가',
save: '저장',
invalid: '유효한 IP 주소를 입력하세요',
noDhcp: '현재 사용 가능한 DHCP DNS가 없습니다',
saved: 'DNS 설정이 저장되었습니다',
saveFailed: 'DNS 설정 저장 실패',
unsaved: '저장되지 않은 변경사항',
maxServers: '최대 {{count}}개의 DNS 서버가 허용됩니다',
dnsServers: 'DNS 서버',
dhcpServersDescription: 'DNS 서버는 DHCP에서 자동으로 가져옵니다.',
manualServersDescription: 'DNS 서버를 수동으로 편집할 수 있습니다.',
networkDetails: '네트워크 세부 정보',
interface: '인터페이스',
ipAddress: 'IP 주소',
subnetMask: '서브넷 마스크',
router: '라우터',
none: '없음'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -182,16 +182,57 @@ const nb = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Nettverk',
wifi: {
title: 'Wi-Fi',
description: 'Konfigurer Wi-Fi',
apMode: 'AP-modus er aktivert, koble til Wi-Fi ved å skanne QR-koden',
connect: 'Koble til Wi-Fi',
connectDesc1: 'Skriv inn nettverkets SSID og passord',
connectDesc2: 'Skriv inn passordet for å koble til dette nettverket',
disconnect: 'Er du sikker på at du vil koble fra nettverket?',
failed: 'Tilkobling mislyktes, prøv igjen.',
ssid: 'Navn',
password: 'Passord',
joinBtn: 'Koble til',
confirmBtn: 'OK',
cancelBtn: 'Avbryt'
},
tls: {
description: 'Aktiver HTTPS-protokoll',
tip: 'Merk: Bruk av HTTPS kan øke forsinkelsen, spesielt i MJPEG-videomodus.'
},
dns: {
title: 'DNS',
description: 'Konfigurer DNS-servere for NanoKVM',
mode: 'Modus',
dhcp: 'DHCP',
manual: 'Manuell',
add: 'Legg til DNS',
save: 'Lagre',
invalid: 'Skriv inn en gyldig IP-adresse',
noDhcp: 'Ingen DHCP-DNS er tilgjengelig nå',
saved: 'DNS-innstillinger lagret',
saveFailed: 'Kunne ikke lagre DNS-innstillinger',
unsaved: 'Ulagrede endringer',
maxServers: 'Maksimalt {{count}} DNS-servere er tillatt',
dnsServers: 'DNS-servere',
dhcpServersDescription: 'DNS-servere hentes automatisk fra DHCP.',
manualServersDescription: 'DNS-servere kan redigeres manuelt.',
networkDetails: 'Nettverksdetaljer',
interface: 'Grensesnitt',
ipAddress: 'IP-adresse',
subnetMask: 'Subnettmaske',
router: 'Ruter',
none: 'Ingen'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -182,16 +182,57 @@ const nl = {
1800: '30 min',
3600: '1 uur'
},
wifi: {
title: 'Wifi',
description: 'Configureer wifi',
setBtn: 'Config'
},
disk: 'Virtuele schijf',
diskDesc: 'Koppel virtuele U-schijf aan de externe host',
network: 'Virtueel Netwerk',
networkDesc: 'Koppel virtueel netwerk kaart aan de externe host'
},
network: {
title: 'Netwerk',
wifi: {
title: 'Wifi',
description: 'Configureer wifi',
apMode: 'AP-modus is ingeschakeld, maak verbinding met wifi door de QR-code te scannen',
connect: 'Wifi verbinden',
connectDesc1: 'Voer de netwerk-SSID en het wachtwoord in',
connectDesc2: 'Voer het wachtwoord in om met dit netwerk te verbinden',
disconnect: 'Weet je zeker dat je de netwerkverbinding wilt verbreken?',
failed: 'Verbinding mislukt, probeer het opnieuw.',
ssid: 'Naam',
password: 'Wachtwoord',
joinBtn: 'Verbinden',
confirmBtn: 'OK',
cancelBtn: 'Annuleren'
},
tls: {
description: 'HTTPS-protocol inschakelen',
tip: 'Let op: HTTPS gebruiken kan de latentie verhogen, vooral in MJPEG-videomodus.'
},
dns: {
title: 'DNS',
description: 'Configureer DNS-servers voor NanoKVM',
mode: 'Modus',
dhcp: 'DHCP',
manual: 'Handmatig',
add: 'DNS toevoegen',
save: 'Opslaan',
invalid: 'Voer een geldig IP-adres in',
noDhcp: 'Er is momenteel geen DHCP-DNS beschikbaar',
saved: 'DNS-instellingen opgeslagen',
saveFailed: 'DNS-instellingen opslaan mislukt',
unsaved: 'Niet-opgeslagen wijzigingen',
maxServers: 'Maximaal {{count}} DNS-servers toegestaan',
dnsServers: 'DNS-servers',
dhcpServersDescription: 'DNS-servers worden automatisch via DHCP verkregen.',
manualServersDescription: 'DNS-servers kunnen handmatig worden bewerkt.',
networkDetails: 'Netwerkdetails',
interface: 'Interface',
ipAddress: 'IP-adres',
subnetMask: 'Subnetmasker',
router: 'Router',
none: 'Geen'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -184,16 +184,57 @@ const pl = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Sieć',
wifi: {
title: 'Wi-Fi',
description: 'Skonfiguruj Wi-Fi',
apMode: 'Tryb AP jest włączony, połącz z Wi-Fi skanując kod QR',
connect: 'Połącz Wi-Fi',
connectDesc1: 'Wprowadź SSID sieci i hasło',
connectDesc2: 'Wprowadź hasło, aby połączyć się z tą siecią',
disconnect: 'Czy na pewno chcesz rozłączyć sieć?',
failed: 'Połączenie nie powiodło się, spróbuj ponownie.',
ssid: 'Nazwa',
password: 'Hasło',
joinBtn: 'Połącz',
confirmBtn: 'OK',
cancelBtn: 'Anuluj'
},
tls: {
description: 'Włącz protokół HTTPS',
tip: 'Uwaga: użycie HTTPS może zwiększyć opóźnienie, szczególnie w trybie wideo MJPEG.'
},
dns: {
title: 'DNS',
description: 'Skonfiguruj serwery DNS dla NanoKVM',
mode: 'Tryb',
dhcp: 'DHCP',
manual: 'Ręcznie',
add: 'Dodaj DNS',
save: 'Zapisz',
invalid: 'Wprowadź prawidłowy adres IP',
noDhcp: 'Brak obecnie dostępnego DNS z DHCP',
saved: 'Ustawienia DNS zapisane',
saveFailed: 'Nie udało się zapisać ustawień DNS',
unsaved: 'Niezapisane zmiany',
maxServers: 'Dozwolone jest maksymalnie {{count}} serwerów DNS',
dnsServers: 'Serwery DNS',
dhcpServersDescription: 'Serwery DNS są automatycznie pobierane z DHCP.',
manualServersDescription: 'Serwery DNS można edytować ręcznie.',
networkDetails: 'Szczegóły sieci',
interface: 'Interfejs',
ipAddress: 'Adres IP',
subnetMask: 'Maska podsieci',
router: 'Router',
none: 'Brak'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -226,19 +226,10 @@ const pt_br = {
1800: '30 min',
3600: '1 hora'
},
wifi: {
title: 'Wi-Fi',
description: 'Configurar Wi-Fi',
setBtn: 'Configurar'
},
ssh: {
description: 'Habilitar acesso remoto SSH',
tip: 'Defina uma senha forte antes de habilitar (Conta - Mudar Senha)'
},
tls: {
description: 'Habilitar protocolo HTTPS',
tip: 'Atenção: O uso de HTTPS pode aumentar a latência, especialmente com o modo de vídeo MJPEG.'
},
advanced: 'Configurações Avançadas',
swap: {
title: 'Swap',
@@ -270,6 +261,52 @@ const pt_br = {
okBtn: 'Sim',
cancelBtn: 'Não'
},
network: {
title: 'Rede',
wifi: {
title: 'Wi-Fi',
description: 'Configurar Wi-Fi',
apMode: 'O modo AP está ativado, conecte-se ao Wi-Fi escaneando o QR code',
connect: 'Conectar Wi-Fi',
connectDesc1: 'Digite o SSID da rede e a senha',
connectDesc2: 'Digite a senha para entrar nesta rede',
disconnect: 'Tem certeza de que deseja desconectar a rede?',
failed: 'Falha na conexão, tente novamente.',
ssid: 'Nome',
password: 'Senha',
joinBtn: 'Entrar',
confirmBtn: 'OK',
cancelBtn: 'Cancelar'
},
tls: {
description: 'Habilitar protocolo HTTPS',
tip: 'Atenção: O uso de HTTPS pode aumentar a latência, especialmente com o modo de vídeo MJPEG.'
},
dns: {
title: 'DNS',
description: 'Configurar servidores DNS para o NanoKVM',
mode: 'Modo',
dhcp: 'DHCP',
manual: 'Manual',
add: 'Adicionar DNS',
save: 'Salvar',
invalid: 'Digite um endereço IP válido',
noDhcp: 'Nenhum DNS DHCP está disponível no momento',
saved: 'Configurações de DNS salvas',
saveFailed: 'Falha ao salvar as configurações de DNS',
unsaved: 'Alterações não salvas',
maxServers: 'Máximo de {{count}} servidores DNS permitido',
dnsServers: 'Servidores DNS',
dhcpServersDescription: 'Os servidores DNS são obtidos automaticamente via DHCP.',
manualServersDescription: 'Os servidores DNS podem ser editados manualmente.',
networkDetails: 'Detalhes da rede',
interface: 'Interface',
ipAddress: 'Endereço IP',
subnetMask: 'Máscara de sub-rede',
router: 'Roteador',
none: 'Nenhum'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -231,19 +231,10 @@ const ru = {
1800: '30 мин',
3600: '1 час'
},
wifi: {
title: 'Wi-Fi',
description: 'Настройка Wi-Fi',
setBtn: 'Конфигурация'
},
ssh: {
description: 'Включить удаленный доступ по SSH',
tip: 'Задайте сильный пароль перед включением (Аккаунт - Пароль - Обновить)'
},
tls: {
description: 'Включить HTTPS Protocol',
tip: 'Имейте в виду: использование HTTPS может увеличить задержку, особенно в режиме видео MJPEG.'
},
advanced: 'Дополнительные настройки',
swap: {
title: 'Файл подкачки',
@@ -272,6 +263,52 @@ const ru = {
okBtn: 'Да',
cancelBtn: 'Нет'
},
network: {
title: 'Сеть',
wifi: {
title: 'Wi-Fi',
description: 'Настройка Wi-Fi',
apMode: 'Режим AP включен, подключитесь к Wi-Fi, отсканировав QR-код',
connect: 'Подключить Wi-Fi',
connectDesc1: 'Введите SSID сети и пароль',
connectDesc2: 'Введите пароль для подключения к этой сети',
disconnect: 'Вы уверены, что хотите отключить сеть?',
failed: 'Не удалось подключиться, попробуйте снова.',
ssid: 'Имя',
password: 'Пароль',
joinBtn: 'Подключить',
confirmBtn: 'OK',
cancelBtn: 'Отмена'
},
tls: {
description: 'Включить протокол HTTPS',
tip: 'Имейте в виду: использование HTTPS может увеличить задержку, особенно в режиме видео MJPEG.'
},
dns: {
title: 'DNS',
description: 'Настройка DNS-серверов для NanoKVM',
mode: 'Режим',
dhcp: 'DHCP',
manual: 'Вручную',
add: 'Добавить DNS',
save: 'Сохранить',
invalid: 'Введите допустимый IP-адрес',
noDhcp: 'DNS от DHCP сейчас недоступен',
saved: 'Настройки DNS сохранены',
saveFailed: 'Не удалось сохранить настройки DNS',
unsaved: 'Несохраненные изменения',
maxServers: 'Разрешено не более {{count}} DNS-серверов',
dnsServers: 'DNS-серверы',
dhcpServersDescription: 'DNS-серверы автоматически получаются от DHCP.',
manualServersDescription: 'DNS-серверы можно редактировать вручную.',
networkDetails: 'Сведения о сети',
interface: 'Интерфейс',
ipAddress: 'IP-адрес',
subnetMask: 'Маска подсети',
router: 'Маршрутизатор',
none: 'Нет'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -242,19 +242,10 @@ const se = {
1800: '30 min',
3600: '1 timme'
},
wifi: {
title: 'Wi-Fi',
description: 'Konfigurera Wi-Fi',
setBtn: 'Konfigurera'
},
ssh: {
description: 'Aktivera SSH-fjärråtkomst',
tip: 'Ställ in ett starkt lösenord innan du aktiverar (Konto - Byt lösenord)'
},
tls: {
description: 'Aktivera HTTPS-protokoll',
tip: 'Observera: Användning av HTTPS kan öka fördröjningen, särskilt med MJPEG-läge.'
},
advanced: 'Avancerade inställningar',
swap: {
title: 'Swap',
@@ -286,6 +277,52 @@ const se = {
okBtn: 'Ja',
cancelBtn: 'Nej'
},
network: {
title: 'Nätverk',
wifi: {
title: 'Wi-Fi',
description: 'Konfigurera Wi-Fi',
apMode: 'AP-läge är aktiverat, anslut till Wi-Fi genom att skanna QR-koden',
connect: 'Anslut Wi-Fi',
connectDesc1: 'Ange nätverkets SSID och lösenord',
connectDesc2: 'Ange lösenordet för att ansluta till detta nätverk',
disconnect: 'Är du säker på att du vill koppla från nätverket?',
failed: 'Anslutningen misslyckades, försök igen.',
ssid: 'Namn',
password: 'Lösenord',
joinBtn: 'Anslut',
confirmBtn: 'OK',
cancelBtn: 'Avbryt'
},
tls: {
description: 'Aktivera HTTPS-protokoll',
tip: 'Observera: Användning av HTTPS kan öka fördröjningen, särskilt med MJPEG-läge.'
},
dns: {
title: 'DNS',
description: 'Konfigurera DNS-servrar för NanoKVM',
mode: 'Läge',
dhcp: 'DHCP',
manual: 'Manuell',
add: 'Lägg till DNS',
save: 'Spara',
invalid: 'Ange en giltig IP-adress',
noDhcp: 'Ingen DHCP-DNS är tillgänglig just nu',
saved: 'DNS-inställningar sparade',
saveFailed: 'Det gick inte att spara DNS-inställningar',
unsaved: 'Osparade ändringar',
maxServers: 'Maximalt {{count}} DNS-servrar tillåtna',
dnsServers: 'DNS-servrar',
dhcpServersDescription: 'DNS-servrar hämtas automatiskt från DHCP.',
manualServersDescription: 'DNS-servrar kan redigeras manuellt.',
networkDetails: 'Nätverksdetaljer',
interface: 'Gränssnitt',
ipAddress: 'IP-adress',
subnetMask: 'Subnätmask',
router: 'Router',
none: 'Ingen'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -205,11 +205,6 @@ const th = {
1800: '30 นาที',
3600: '1 ชั่วโมง'
},
wifi: {
title: 'Wi-Fi',
description: 'ตั้งค่า Wi-Fi',
setBtn: 'ยืนยัน'
},
ssh: {
description: 'เปิดใช้งาน SSH',
tip: 'ตั้งรหัสผ่านทีปลอดถัยก่อนเปิดใช้งาน (บัญชี - เปลี่ยนรหัสผ่าน)'
@@ -219,6 +214,52 @@ const th = {
network: 'เครือข่ายจำลอง',
networkDesc: 'เปิดใช้งานอุปกรณ์เครือข่ายจำลอง'
},
network: {
title: 'เครือข่าย',
wifi: {
title: 'Wi-Fi',
description: 'ตั้งค่า Wi-Fi',
apMode: 'เปิดใช้งานโหมด AP แล้ว ให้เชื่อมต่อ Wi-Fi โดยสแกนรหัส QR',
connect: 'เชื่อมต่อ Wi-Fi',
connectDesc1: 'กรุณาใส่ SSID และรหัสผ่านของเครือข่าย',
connectDesc2: 'กรุณาใส่รหัสผ่านเพื่อเข้าร่วมเครือข่ายนี้',
disconnect: 'คุณแน่ใจหรือไม่ว่าต้องการตัดการเชื่อมต่อเครือข่าย?',
failed: 'เชื่อมต่อไม่สำเร็จ กรุณาลองอีกครั้ง',
ssid: 'ชื่อ',
password: 'รหัสผ่าน',
joinBtn: 'เข้าร่วม',
confirmBtn: 'ตกลง',
cancelBtn: 'ยกเลิก'
},
tls: {
description: 'เปิดใช้งานโปรโตคอล HTTPS',
tip: 'โปรดทราบ: การใช้ HTTPS อาจเพิ่มความหน่วง โดยเฉพาะในโหมดวิดีโอ MJPEG'
},
dns: {
title: 'DNS',
description: 'ตั้งค่าเซิร์ฟเวอร์ DNS สำหรับ NanoKVM',
mode: 'โหมด',
dhcp: 'DHCP',
manual: 'กำหนดเอง',
add: 'เพิ่ม DNS',
save: 'บันทึก',
invalid: 'กรุณาใส่ที่อยู่ IP ที่ถูกต้อง',
noDhcp: 'ขณะนี้ไม่มี DHCP DNS ให้ใช้งาน',
saved: 'บันทึกการตั้งค่า DNS แล้ว',
saveFailed: 'บันทึกการตั้งค่า DNS ไม่สำเร็จ',
unsaved: 'มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก',
maxServers: 'อนุญาตเซิร์ฟเวอร์ DNS ได้สูงสุด {{count}} รายการ',
dnsServers: 'เซิร์ฟเวอร์ DNS',
dhcpServersDescription: 'เซิร์ฟเวอร์ DNS จะได้รับจาก DHCP โดยอัตโนมัติ',
manualServersDescription: 'สามารถแก้ไขเซิร์ฟเวอร์ DNS ได้ด้วยตนเอง',
networkDetails: 'รายละเอียดเครือข่าย',
interface: 'อินเทอร์เฟซ',
ipAddress: 'ที่อยู่ IP',
subnetMask: 'Subnet Mask',
router: 'เราเตอร์',
none: 'ไม่มี'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -230,19 +230,10 @@ const tr = {
1800: '30 dakika',
3600: '1 saat'
},
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi ayarlayın',
setBtn: 'Ayarla'
},
ssh: {
description: 'Güvenli Kabuk Bağlantısı (SSH) aktif et',
tip: 'Aktifleştirmeden önce güçlü bir şifreye sahip olduğunuzdan emin olun (Hesap - Şifremi Değiştir)'
},
tls: {
description: 'HTTPS protokolünü etkinleştir',
tip: 'HTTPS protokolü bağlantıda gecikmeye sebep olabilir, özellikle MJPEG görüntü modu ile.'
},
advanced: 'Gelişmiş Ayarlar',
swap: {
title: 'Swap',
@@ -274,6 +265,52 @@ const tr = {
okBtn: 'Evet',
cancelBtn: 'Hayır'
},
network: {
title: 'Ağ',
wifi: {
title: 'Wi-Fi',
description: 'Wi-Fi ayarlayın',
apMode: 'AP modu etkin, QR kodu tarayarak Wi-Fi\'ye bağlanın',
connect: 'Wi-Fi\'ye bağlan',
connectDesc1: 'Lütfen ağ SSID\'sini ve parolayı girin',
connectDesc2: 'Bu ağa katılmak için parolayı girin',
disconnect: 'Ağ bağlantısını kesmek istediğinizden emin misiniz?',
failed: 'Bağlantı başarısız, lütfen tekrar deneyin.',
ssid: 'Ad',
password: 'Parola',
joinBtn: 'Katıl',
confirmBtn: 'Tamam',
cancelBtn: 'İptal'
},
tls: {
description: 'HTTPS protokolünü etkinleştir',
tip: 'HTTPS protokolü bağlantıda gecikmeye sebep olabilir, özellikle MJPEG görüntü modu ile.'
},
dns: {
title: 'DNS',
description: 'NanoKVM için DNS sunucularını yapılandır',
mode: 'Mod',
dhcp: 'DHCP',
manual: 'Manuel',
add: 'DNS ekle',
save: 'Kaydet',
invalid: 'Geçerli bir IP adresi girin',
noDhcp: 'Şu anda DHCP DNS mevcut değil',
saved: 'DNS ayarları kaydedildi',
saveFailed: 'DNS ayarları kaydedilemedi',
unsaved: 'Kaydedilmemiş değişiklikler',
maxServers: 'En fazla {{count}} DNS sunucusuna izin verilir',
dnsServers: 'DNS Sunucuları',
dhcpServersDescription: 'DNS sunucuları DHCP\'den otomatik olarak alınır.',
manualServersDescription: 'DNS sunucuları manuel olarak düzenlenebilir.',
networkDetails: 'Ağ Ayrıntıları',
interface: 'Arayüz',
ipAddress: 'IP Adresi',
subnetMask: 'Alt Ağ Maskesi',
router: 'Yönlendirici',
none: 'Yok'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -227,19 +227,10 @@ const uk = {
1800: '30 хв',
3600: '1 година'
},
wifi: {
title: 'Wi-Fi',
description: 'Налаштування Wi-Fi',
setBtn: 'Конфігурація'
},
ssh: {
description: 'Увімкніть віддалений доступ по SSH',
tip: 'Перед ввімкненням встановіть надійний пароль (Обліковий запис - Зміна пароля)'
},
tls: {
description: 'Увімкнути протокол HTTPS',
tip: 'Будьте в курсі: Використання HTTPS може збільшити затримку, особливо в режимі відео MJPEG.'
},
advanced: 'Розширені налаштування',
swap: {
title: 'Swap (файл підкачки)',
@@ -268,6 +259,52 @@ const uk = {
okBtn: 'Так',
cancelBtn: 'Ні'
},
network: {
title: 'Мережа',
wifi: {
title: 'Wi-Fi',
description: 'Налаштування Wi-Fi',
apMode: 'Режим AP увімкнено, підключіться до Wi-Fi, відсканувавши QR-код',
connect: 'Підключити Wi-Fi',
connectDesc1: 'Введіть SSID мережі та пароль',
connectDesc2: 'Введіть пароль, щоб підключитися до цієї мережі',
disconnect: 'Ви впевнені, що хочете відключити мережу?',
failed: 'Не вдалося підключитися, спробуйте ще раз.',
ssid: 'Назва',
password: 'Пароль',
joinBtn: 'Підключити',
confirmBtn: 'OK',
cancelBtn: 'Скасувати'
},
tls: {
description: 'Увімкнути протокол HTTPS',
tip: 'Будьте в курсі: Використання HTTPS може збільшити затримку, особливо в режимі відео MJPEG.'
},
dns: {
title: 'DNS',
description: 'Налаштування DNS-серверів для NanoKVM',
mode: 'Режим',
dhcp: 'DHCP',
manual: 'Вручну',
add: 'Додати DNS',
save: 'Зберегти',
invalid: 'Введіть дійсну IP-адресу',
noDhcp: 'Наразі DHCP DNS недоступний',
saved: 'Налаштування DNS збережено',
saveFailed: 'Не вдалося зберегти налаштування DNS',
unsaved: 'Незбережені зміни',
maxServers: 'Дозволено щонайбільше {{count}} DNS-серверів',
dnsServers: 'DNS-сервери',
dhcpServersDescription: 'DNS-сервери автоматично отримуються з DHCP.',
manualServersDescription: 'DNS-сервери можна редагувати вручну.',
networkDetails: 'Відомості про мережу',
interface: 'Інтерфейс',
ipAddress: 'IP-адреса',
subnetMask: 'Маска підмережі',
router: 'Маршрутизатор',
none: 'Немає'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -181,16 +181,57 @@ const vi = {
1800: '30 min',
3600: '1 hour'
},
wifi: {
title: 'Wi-Fi',
description: 'Configure Wi-Fi',
setBtn: 'Config'
},
disk: 'Virtual Disk',
diskDesc: 'Mount virtual U-disk on the remote host',
network: 'Virtual Network',
networkDesc: 'Mount virtual network card on the remote host'
},
network: {
title: 'Mạng',
wifi: {
title: 'Wi-Fi',
description: 'Cấu hình Wi-Fi',
apMode: 'Chế độ AP đang bật, hãy kết nối Wi-Fi bằng cách quét mã QR',
connect: 'Kết nối Wi-Fi',
connectDesc1: 'Vui lòng nhập SSID mạng và mật khẩu',
connectDesc2: 'Vui lòng nhập mật khẩu để tham gia mạng này',
disconnect: 'Bạn có chắc muốn ngắt kết nối mạng không?',
failed: 'Kết nối thất bại, vui lòng thử lại.',
ssid: 'Tên',
password: 'Mật khẩu',
joinBtn: 'Tham gia',
confirmBtn: 'OK',
cancelBtn: 'Hủy'
},
tls: {
description: 'Bật giao thức HTTPS',
tip: 'Lưu ý: Sử dụng HTTPS có thể tăng độ trễ, đặc biệt trong chế độ video MJPEG.'
},
dns: {
title: 'DNS',
description: 'Cấu hình máy chủ DNS cho NanoKVM',
mode: 'Chế độ',
dhcp: 'DHCP',
manual: 'Thủ công',
add: 'Thêm DNS',
save: 'Lưu',
invalid: 'Vui lòng nhập địa chỉ IP hợp lệ',
noDhcp: 'Hiện không có DNS DHCP khả dụng',
saved: 'Đã lưu cài đặt DNS',
saveFailed: 'Không thể lưu cài đặt DNS',
unsaved: 'Thay đổi chưa lưu',
maxServers: 'Cho phép tối đa {{count}} máy chủ DNS',
dnsServers: 'Máy chủ DNS',
dhcpServersDescription: 'Máy chủ DNS được tự động lấy từ DHCP.',
manualServersDescription: 'Có thể chỉnh sửa máy chủ DNS thủ công.',
networkDetails: 'Chi tiết mạng',
interface: 'Giao diện',
ipAddress: 'Địa chỉ IP',
subnetMask: 'Mặt nạ mạng con',
router: 'Bộ định tuyến',
none: 'Không có'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -276,29 +276,10 @@ const zh = {
1800: '30分钟',
3600: '1小时'
},
wifi: {
title: 'Wi-Fi',
description: '配置 Wi-Fi 信息',
apMode: 'AP 模式已启用,该模式下仅支持扫描二维码配网',
connect: '连接 Wi-Fi',
connectDesc1: '请输入网络名称和密码',
connectDesc2: '请输入密码以连接此网络',
disconnect: '是否要断开该网络连接?',
failed: '连接失败,请重试',
ssid: '名称',
password: '密码',
joinBtn: '加入',
confirmBtn: '确定',
cancelBtn: '取消'
},
ssh: {
description: '启用 SSH 远程访问',
tip: '启用前请务必设置强密码(帐号 - 修改密码)'
},
tls: {
description: '启用 HTTPS 协议',
tip: '注意:使用 HTTPS 可能导致延迟增加,特别是在 MJPEG 视频模式下。'
},
advanced: '高级设置',
swap: {
disable: '禁用',
@@ -339,6 +320,52 @@ const zh = {
okBtn: '是',
cancelBtn: '否'
},
network: {
title: '网络',
wifi: {
title: 'Wi-Fi',
description: '配置 Wi-Fi 信息',
apMode: 'AP 模式已启用,该模式下仅支持扫描二维码配网',
connect: '连接 Wi-Fi',
connectDesc1: '请输入网络名称和密码',
connectDesc2: '请输入密码以连接此网络',
disconnect: '是否要断开该网络连接?',
failed: '连接失败,请重试',
ssid: '名称',
password: '密码',
joinBtn: '加入',
confirmBtn: '确定',
cancelBtn: '取消'
},
tls: {
description: '启用 HTTPS 协议',
tip: '注意:使用 HTTPS 可能导致延迟增加,特别是在 MJPEG 视频模式下。'
},
dns: {
title: 'DNS',
description: '配置 NanoKVM 使用的 DNS 服务器',
mode: '模式',
dhcp: 'DHCP',
manual: '手动',
add: '添加 DNS',
save: '保存',
invalid: '请输入有效的 IP 地址',
noDhcp: '当前未获取到 DHCP DNS',
saved: 'DNS 设置已保存',
saveFailed: '保存 DNS 设置失败',
unsaved: '有未保存的更改',
maxServers: '最多允许 {{count}} 个 DNS 服务器',
dnsServers: 'DNS 服务器',
dhcpServersDescription: 'DNS 服务器由 DHCP 自动获取。',
manualServersDescription: 'DNS 服务器可以手动编辑。',
networkDetails: '网络详情',
interface: '接口',
ipAddress: 'IP 地址',
subnetMask: '子网掩码',
router: '路由器',
none: '无'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -258,30 +258,10 @@ const zh_tw = {
1800: '30 分鐘',
3600: '1 小時'
},
wifi: {
title: 'Wi-Fi',
description: '設定 Wi-Fi',
setBtn: '設定',
apMode: 'AP 模式已啟用,該模式下僅支援掃描 QRCode 配網',
connect: '連線 Wi-Fi',
connectDesc1: '請輸入網路名稱和密碼',
connectDesc2: '請輸入密碼以連線此網路',
disconnect: '是否要中斷該網路連線?',
failed: '連線失敗,請重試',
ssid: 'SSID 名稱',
password: '密碼',
joinBtn: '加入',
confirmBtn: '確定',
cancelBtn: '取消'
},
ssh: {
description: '啟用 SSH 伺服器',
tip: '啟用前請務必設定強密碼(帳號 - 更改密碼)'
},
tls: {
description: '啟用 HTTPS 協議',
tip: '啟用 HTTPS 可以提高安全性,但可能會增加傳輸延遲,特別是使用 MJPEG 格式傳輸時。'
},
advanced: '進階設定',
swap: {
title: 'Swap',
@@ -325,6 +305,52 @@ const zh_tw = {
okBtn: '確定',
cancelBtn: '取消'
},
network: {
title: '網路',
wifi: {
title: 'Wi-Fi',
description: '設定 Wi-Fi',
apMode: 'AP 模式已啟用,該模式下僅支援掃描 QRCode 配網',
connect: '連線 Wi-Fi',
connectDesc1: '請輸入網路名稱和密碼',
connectDesc2: '請輸入密碼以連線此網路',
disconnect: '是否要中斷該網路連線?',
failed: '連線失敗,請重試',
ssid: 'SSID 名稱',
password: '密碼',
joinBtn: '加入',
confirmBtn: '確定',
cancelBtn: '取消'
},
tls: {
description: '啟用 HTTPS 協議',
tip: '啟用 HTTPS 可以提高安全性,但可能會增加傳輸延遲,特別是使用 MJPEG 格式傳輸時。'
},
dns: {
title: 'DNS',
description: '設定 NanoKVM 使用的 DNS 伺服器',
mode: '模式',
dhcp: 'DHCP',
manual: '手動',
add: '新增 DNS',
save: '儲存',
invalid: '請輸入有效的 IP 位址',
noDhcp: '目前未取得 DHCP DNS',
saved: 'DNS 設定已儲存',
saveFailed: '儲存 DNS 設定失敗',
unsaved: '有未儲存的變更',
maxServers: '最多允許 {{count}} 個 DNS 伺服器',
dnsServers: 'DNS 伺服器',
dhcpServersDescription: 'DNS 伺服器由 DHCP 自動取得。',
manualServersDescription: 'DNS 伺服器可以手動編輯。',
networkDetails: '網路詳細資訊',
interface: '介面',
ipAddress: 'IP 位址',
subnetMask: '子網路遮罩',
router: '路由器',
none: '無'
}
},
tailscale: {
title: 'Tailscale',
memory: {

View File

@@ -8,9 +8,7 @@ import { MouseJiggler } from './mouse-jiggler.tsx';
import { Oled } from './oled.tsx';
import { Reboot } from './reboot.tsx';
import { Ssh } from './ssh.tsx';
import { Tls } from './tls.tsx';
import { VirtualDevices } from './virtual-devices.tsx';
import { Wifi } from './wifi.tsx';
export const Device = () => {
const { t } = useTranslation();
@@ -21,7 +19,6 @@ export const Device = () => {
<Divider className="opacity-50" />
<div className="flex flex-col space-y-8">
<Tls />
<Ssh />
<Mdns />
<Hdmi />
@@ -31,7 +28,6 @@ export const Device = () => {
<Divider className="opacity-50" />
<Oled />
<Wifi />
<MouseJiggler />
<Divider className="opacity-50" />

View File

@@ -1,10 +1,11 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Badge, Modal, Tooltip } from 'antd';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import {
BadgeInfoIcon,
CircleArrowUpIcon,
NetworkIcon,
PaletteIcon,
SettingsIcon,
SmartphoneIcon,
@@ -24,6 +25,7 @@ import { About } from './about';
import { Account } from './account';
import { Appearance } from './appearance';
import { Device } from './device';
import { Network } from './network';
import { Tailscale } from './tailscale';
import { Update } from './update';
@@ -33,6 +35,7 @@ export const Settings = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [currentTab, setCurrentTab] = useState('about');
const scrollViewportRef = useRef<HTMLDivElement>(null);
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
@@ -42,6 +45,7 @@ export const Settings = () => {
{ id: 'about', icon: <BadgeInfoIcon size={16} />, component: <About /> },
{ id: 'appearance', icon: <PaletteIcon size={16} />, component: <Appearance /> },
{ id: 'device', icon: <SmartphoneIcon size={16} />, component: <Device /> },
{ id: 'network', icon: <NetworkIcon size={16} />, component: <Network /> },
{
id: 'tailscale',
icon: <TailscaleIcon />,
@@ -62,6 +66,10 @@ export const Settings = () => {
}
}, []);
useEffect(() => {
scrollViewportRef.current?.scrollTo({ top: 0, left: 0 });
}, [currentTab]);
function checkForUpdates() {
api.getVersion().then((rsp: any) => {
if (rsp.code !== 0) {
@@ -162,7 +170,10 @@ export const Settings = () => {
))}
</div>
<ScrollArea className="h-full w-full rounded-r-lg bg-neutral-900/50 px-3">
<ScrollArea
viewportRef={scrollViewportRef}
className="h-full w-full rounded-r-lg bg-neutral-900/50 px-3"
>
<div className="flex h-full w-full justify-center">
<div className="w-full max-w-[600px] pb-10 pt-14">
<>{tabs.find((tab) => tab.id === currentTab)?.component}</>

View File

@@ -0,0 +1,439 @@
import { useEffect, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import { Button, Input, Segmented } from 'antd';
import { CheckIcon, PlusIcon, XIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import * as api from '@/api/network.ts';
import type { DNSMode } from '@/api/network.ts';
type DNSState = {
mode: DNSMode;
servers: string[];
dhcp: string[];
info: DNSInfo;
};
type DNSInfo = {
interface?: string;
type?: string;
address?: string;
subnetMask?: string;
gateway?: string;
};
const maxServers = 6;
function formatInterface(info: DNSInfo) {
if (!info.interface) return '';
if (!info.type) return info.interface;
return `${info.type} (${info.interface})`;
}
function normalizeServers(servers: string[]) {
const seen = new Set<string>();
const normalized: string[] = [];
for (const server of servers) {
const value = normalizeServer(server);
if (!value || seen.has(value)) continue;
seen.add(value);
normalized.push(value);
}
return normalized;
}
function normalizeServer(server: string) {
return server.split('#')[0].trim().split(/\s+/)[0] || '';
}
function isValidIP(value: string) {
return isValidIPv4(value) || isValidIPv6(value);
}
function isValidIPv4(value: string) {
const parts = value.split('.');
if (parts.length !== 4) return false;
return parts.every((part) => {
if (!/^\d+$/.test(part)) return false;
if (part.length > 1 && part.startsWith('0')) return false;
const number = Number(part);
return number >= 0 && number <= 255;
});
}
function isValidIPv6(value: string) {
if (!value.includes(':')) return false;
try {
new URL(`http://[${value}]/`);
return true;
} catch {
return false;
}
}
const Panel = ({
title,
description,
children
}: {
title: string;
description?: string;
children: ReactNode;
}) => {
return (
<div className="overflow-hidden rounded-xl bg-neutral-800/50">
<div className="px-4 pb-1.5 pt-3">
<div className="font-semibold text-neutral-100">{title}</div>
{description && (
<div className="mt-0.5 text-xs leading-snug text-neutral-500">{description}</div>
)}
</div>
<div>{children}</div>
</div>
);
};
const InfoRow = ({
label,
value,
isLast = false
}: {
label: string;
value?: string;
isLast?: boolean;
}) => {
return (
<div className="px-4">
<div
className={`flex min-h-[44px] items-center justify-between ${
isLast ? '' : 'border-b border-neutral-700/50'
}`}
>
<span className="text-sm text-neutral-300">{label}</span>
<span className="max-w-[330px] break-all text-right text-sm text-neutral-500">
{value || '-'}
</span>
</div>
</div>
);
};
const ServerList = ({ servers }: { servers: string[] }) => {
const { t } = useTranslation();
if (!servers.length) {
return (
<div className="px-4 py-3 text-sm text-neutral-500">{t('settings.network.dns.none')}</div>
);
}
return (
<div>
{servers.map((server, index) => (
<div key={`${server}-${index}`} className="px-4">
<div
className={`min-h-[44px] py-2.5 text-sm text-neutral-400 ${
index === servers.length - 1 ? '' : 'border-b border-neutral-700/50'
}`}
>
{server}
</div>
</div>
))}
</div>
);
};
const EditableServerRow = ({
value,
autoFocus,
onChange,
onRemove
}: {
value: string;
autoFocus: boolean;
onChange: (value: string) => void;
onRemove: () => void;
}) => {
const inputRef = useRef<any>(null);
const normalized = normalizeServer(value);
const isInvalid = normalized !== '' && !isValidIP(normalized);
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus();
}
}, [autoFocus]);
return (
<div className="group px-4 py-1.5">
<div className="flex items-center gap-2">
<Input
ref={inputRef}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="0.0.0.0"
status={isInvalid ? 'error' : undefined}
/>
<Button
size="small"
shape="circle"
icon={<XIcon size={14} />}
onClick={onRemove}
className="opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
/>
</div>
</div>
);
};
export const DNS = () => {
const { t } = useTranslation();
const [mode, setMode] = useState<DNSMode>('dhcp');
const [originalMode, setOriginalMode] = useState<DNSMode>('dhcp');
const [servers, setServers] = useState<string[]>([]);
const [originalServers, setOriginalServers] = useState<string[]>([]);
const [dhcp, setDHCP] = useState<string[]>([]);
const [info, setInfo] = useState<DNSInfo>({});
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const [focusNewRow, setFocusNewRow] = useState(false);
useEffect(() => {
getDNS();
}, []);
async function getDNS(showLoading = true) {
if (showLoading) setIsLoading(true);
try {
const rsp = await api.getDNS();
if (rsp.code !== 0) {
setError(rsp.msg);
return;
}
const data = rsp.data as DNSState;
const fetchedMode = data.mode || 'dhcp';
setMode(fetchedMode);
setOriginalMode(fetchedMode);
const fetchedServers = data.servers?.filter(Boolean) || [];
setServers(fetchedServers);
setOriginalServers(fetchedServers);
setDHCP(data.dhcp || []);
setInfo(data.info || {});
} catch (err) {
console.log(err);
} finally {
if (showLoading) setIsLoading(false);
}
}
async function save() {
if (isSaving) return;
setMessage('');
setError('');
const normalized = normalizeServers(servers);
if (mode === 'manual' && normalized.length === 0) {
setError(t('settings.network.dns.invalid'));
return;
}
if (mode === 'manual' && normalized.some((server) => !isValidIP(server))) {
setError(t('settings.network.dns.invalid'));
return;
}
setIsSaving(true);
try {
const rsp = await api.setDNS(mode, mode === 'manual' ? normalized : []);
if (rsp.code !== 0) {
setError(rsp.msg || t('settings.network.dns.saveFailed'));
return;
}
setServers(normalized);
setOriginalServers(normalized);
setOriginalMode(mode);
await getDNS(false);
setMessage(t('settings.network.dns.saved'));
} catch (err) {
console.log(err);
setError(t('settings.network.dns.saveFailed'));
} finally {
setIsSaving(false);
}
}
function addServer() {
if (servers.length >= maxServers) return;
setMessage('');
setError('');
setServers([...servers, '']);
setFocusNewRow(true);
}
function removeServer(index: number) {
setMessage('');
setError('');
setServers(servers.filter((_, i) => i !== index));
}
function updateServer(index: number, value: string) {
setMessage('');
setError('');
const updated = [...servers];
updated[index] = value;
setServers(updated);
}
const normalizedServers = normalizeServers(servers);
const hasInvalidServer =
mode === 'manual' &&
servers.some((server) => {
const val = normalizeServer(server);
return val !== '' && !isValidIP(val);
});
const isExceedMax = mode === 'manual' && normalizedServers.length > maxServers;
const hasChanges =
mode !== originalMode ||
normalizedServers.join(',') !== normalizeServers(originalServers).join(',');
const statusText = error || message || (hasChanges ? t('settings.network.dns.unsaved') : '');
const statusColor = error ? 'text-red-400' : message ? 'text-green-400' : 'text-yellow-400/80';
const serversDescription =
mode === 'dhcp'
? t('settings.network.dns.dhcpServersDescription')
: t('settings.network.dns.manualServersDescription');
const canAdd = !isLoading && !isSaving && servers.length < maxServers;
return (
<div className="flex flex-col space-y-5">
{/* Header row: title + segmented control */}
<div className="flex items-center justify-between">
<div className="flex flex-col space-y-1">
<span>{t('settings.network.dns.title')}</span>
<span className="text-xs text-neutral-500">{t('settings.network.dns.description')}</span>
</div>
<Segmented
disabled={isLoading || isSaving}
value={mode}
onChange={(val) => {
setMode(val as DNSMode);
setMessage('');
setError('');
}}
options={[
{ label: t('settings.network.dns.dhcp'), value: 'dhcp' },
{ label: t('settings.network.dns.manual'), value: 'manual' }
]}
/>
</div>
<div className="space-y-5">
<Panel title={t('settings.network.dns.networkDetails')}>
<InfoRow label={t('settings.network.dns.interface')} value={formatInterface(info)} />
<InfoRow label={t('settings.network.dns.ipAddress')} value={info.address} />
<InfoRow label={t('settings.network.dns.subnetMask')} value={info.subnetMask} />
<InfoRow label={t('settings.network.dns.router')} value={info.gateway} isLast />
</Panel>
<Panel title={t('settings.network.dns.dnsServers')} description={serversDescription}>
{mode === 'manual' ? (
<div>
{servers.length === 0 ? (
<div className="px-4 py-3 text-sm text-neutral-500">
{t('settings.network.dns.none')}
</div>
) : (
servers.map((server, index) => (
<EditableServerRow
key={index}
value={server}
autoFocus={focusNewRow && index === servers.length - 1}
onChange={(val) => updateServer(index, val)}
onRemove={() => removeServer(index)}
/>
))
)}
{/* Add server button */}
{canAdd && (
<div className="px-4 py-1.5 pb-3">
<div className="flex items-center gap-2">
<Button
type="dashed"
className="flex-1"
icon={<PlusIcon size={14} />}
onClick={addServer}
>
{t('settings.network.dns.add')}
</Button>
<Button
type="text"
size="small"
className="invisible shrink-0"
icon={<XIcon size={14} />}
/>
</div>
</div>
)}
{/* Validation hints */}
{(hasInvalidServer || isExceedMax) && (
<div className="space-y-1 px-4 pb-3">
{hasInvalidServer && (
<div className="text-xs text-red-400">{t('settings.network.dns.invalid')}</div>
)}
{isExceedMax && (
<div className="text-xs text-red-400">
{t('settings.network.dns.maxServers', { count: maxServers })}
</div>
)}
</div>
)}
</div>
) : (
<ServerList servers={dhcp} />
)}
</Panel>
</div>
{/* Footer: status + save button */}
{(hasChanges || statusText) && (
<div className="flex items-center justify-between">
<span className={`text-xs ${statusColor}`}>{statusText}</span>
<Button
type={hasChanges ? 'primary' : 'default'}
icon={message ? <CheckIcon size={14} /> : undefined}
loading={isSaving}
disabled={
isLoading || (!hasChanges && !hasInvalidServer) || hasInvalidServer || isExceedMax
}
onClick={save}
>
{t('settings.network.dns.save')}
</Button>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,26 @@
import { Divider } from 'antd';
import { useTranslation } from 'react-i18next';
import { DNS } from './dns.tsx';
import { Tls } from './tls.tsx';
import { Wifi } from './wifi.tsx';
export const Network = () => {
const { t } = useTranslation();
return (
<>
<div className="text-base">{t('settings.network.title')}</div>
<Divider className="opacity-50" />
<div className="flex flex-col space-y-8">
<Tls />
<Wifi />
</div>
<Divider className="opacity-50" style={{ margin: '32px 0' }} />
<DNS />
</>
);
};

View File

@@ -52,7 +52,7 @@ export const Tls = () => {
<span>HTTPS</span>
<Tooltip
title={t('settings.device.tls.tip')}
title={t('settings.network.tls.tip')}
className="cursor-pointer"
placement="right"
styles={{ root: { maxWidth: '400px' } }}
@@ -60,7 +60,7 @@ export const Tls = () => {
<CircleAlertIcon className="text-neutral-500" size={14} />
</Tooltip>
</div>
<span className="text-xs text-neutral-500">{t('settings.device.tls.description')}</span>
<span className="text-xs text-neutral-500">{t('settings.network.tls.description')}</span>
</div>
<Switch checked={isEnabled} loading={isLoading} onChange={update} />

View File

@@ -56,7 +56,7 @@ export const Wifi = () => {
const rsp = await api.connectWifi(ssid, password);
if (rsp.code !== 0) {
console.log(rsp.msg);
setMessage(t('settings.device.wifi.failed'));
setMessage(t('settings.network.wifi.failed'));
getWiFi();
return;
}
@@ -109,9 +109,9 @@ export const Wifi = () => {
if (isAPMode) {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<span>{t('settings.device.wifi.title')}</span>
<span className="text-xs text-neutral-500">{t('settings.device.wifi.apMode')}</span>
<div className="flex flex-col space-y-1">
<span>{t('settings.network.wifi.title')}</span>
<span className="text-xs text-neutral-500">{t('settings.network.wifi.apMode')}</span>
</div>
<Button shape="round" size="small" disabled>
@@ -127,8 +127,10 @@ export const Wifi = () => {
<>
<div className="flex items-center justify-between">
<div className="flex flex-col space-y-1">
<span>{t('settings.device.wifi.title')}</span>
<span className="text-xs text-neutral-500">{t('settings.device.wifi.description')}</span>
<span>{t('settings.network.wifi.title')}</span>
<span className="text-xs text-neutral-500">
{connectedWiFi ? connectedWiFi : t('settings.network.wifi.description')}
</span>
</div>
<Button
@@ -149,8 +151,8 @@ export const Wifi = () => {
centered={true}
onOk={connect}
onCancel={closeModal}
okText={t('settings.device.wifi.joinBtn')}
cancelText={t('settings.device.wifi.cancelBtn')}
okText={t('settings.network.wifi.joinBtn')}
cancelText={t('settings.network.wifi.cancelBtn')}
confirmLoading={status === 'connecting'}
>
{/* title */}
@@ -161,9 +163,9 @@ export const Wifi = () => {
{!connectedWiFi ? (
<div className="flex flex-col">
<span className="text-lg font-bold">{t('settings.device.wifi.connect')}</span>
<span className="text-lg font-bold">{t('settings.network.wifi.connect')}</span>
<span className="text-xs text-neutral-400">
{t('settings.device.wifi.connectDesc1')}
{t('settings.network.wifi.connectDesc1')}
</span>
</div>
) : (
@@ -188,14 +190,14 @@ export const Wifi = () => {
value={ssid}
style={{ width: '300px' }}
prefix={<WifiOutlined />}
placeholder={t('settings.device.wifi.ssid')}
placeholder={t('settings.network.wifi.ssid')}
onChange={(e) => setSsid(e.target.value)}
/>
<Input.Password
value={password}
style={{ width: '300px' }}
prefix={<LockOutlined />}
placeholder={t('settings.device.wifi.password')}
placeholder={t('settings.network.wifi.password')}
onChange={(e) => setPassword(e.target.value)}
/>