diff --git a/server/proto/network.go b/server/proto/network.go index e6cf9ff..ec6238e 100644 --- a/server/proto/network.go +++ b/server/proto/network.go @@ -28,3 +28,25 @@ type ConnectWifiReq struct { Ssid string `validate:"required"` Password string `valid:"required"` } + +type GetDNSRsp struct { + Mode string `json:"mode"` + Servers []string `json:"servers"` + Effective []string `json:"effective"` + DHCP []string `json:"dhcp"` + Info DNSInfo `json:"info"` +} + +type SetDNSReq struct { + Mode string `json:"mode" validate:"required,oneof=manual dhcp"` + Servers []string `json:"servers"` +} + +type DNSInfo struct { + Interface string `json:"interface"` + Type string `json:"type"` + Address string `json:"address"` + SubnetMask string `json:"subnetMask"` + Gateway string `json:"gateway"` + SearchDomains []string `json:"searchDomains"` +} diff --git a/server/router/network.go b/server/router/network.go index 45937ec..9aeb4b6 100644 --- a/server/router/network.go +++ b/server/router/network.go @@ -10,8 +10,8 @@ import ( func networkRouter(r *gin.Engine) { service := network.NewService() - r.POST("/api/network/wifi", service.ConnectWifiNoAuth) // connect Wi-Fi without auth (only available in ap mode) - r.POST("/api/network/wifi/verify", service.VerifyApLogin) // verify ap login + r.POST("/api/network/wifi", service.ConnectWifiNoAuth) // connect Wi-Fi without auth (only available in ap mode) + r.POST("/api/network/wifi/verify", service.VerifyApLogin) // verify ap login api := r.Group("/api").Use(middleware.CheckToken()) @@ -23,4 +23,7 @@ func networkRouter(r *gin.Engine) { api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information api.POST("/network/wifi/connect", service.ConnectWifi) // connect Wi-Fi api.POST("/network/wifi/disconnect", service.DisconnectWifi) // disconnect Wi-Fi + + api.GET("/network/dns", service.GetDNS) // get DNS configuration + api.POST("/network/dns", service.SetDNS) // set DNS configuration } diff --git a/server/service/network/dns.go b/server/service/network/dns.go new file mode 100644 index 0000000..74c2e78 --- /dev/null +++ b/server/service/network/dns.go @@ -0,0 +1,614 @@ +package network + +import ( + "bufio" + _ "embed" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "NanoKVM-Server/proto" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +const ( + dnsModeManual = "manual" + dnsModeDHCP = "dhcp" + + dnsConfigDir = "/etc/kvm/network" + dnsModeFile = "/etc/kvm/network/dns.mode" + dnsServersFile = "/etc/kvm/network/dns.servers" + bootResolvFile = "/boot/resolv.conf" + bootResolvBackup = "/boot/resolv.conf.manual.bak" + etcResolvFile = "/etc/resolv.conf" + dhcpResolvFile = "/etc/resolv.conf.dhcp" + udhcpcHookFile = "/usr/share/udhcpc/default.script.d/99-nanokvm-dns" + + maxDNSServers = 6 +) + +//go:embed scripts/99-nanokvm-dns +var udhcpcDNSHook string + +type resolvConfig struct { + Servers []string + SearchDomains []string +} + +func (s *Service) GetDNS(c *gin.Context) { + var rsp proto.Response + + mode := currentDNSMode() + + servers := readManualDNSServers() + if len(servers) == 0 { + servers, _ = parseResolvConf(bootResolvFile) + } + if len(servers) == 0 { + servers, _ = parseResolvConf(bootResolvBackup) + } + + effective, _ := parseResolvConf(etcResolvFile) + dhcpConfig, _ := readDHCPResolvConfig(mode == dnsModeDHCP) + dhcp := dhcpConfig.Servers + info := getDNSInfo() + + rsp.OkRspWithData(c, &proto.GetDNSRsp{ + Mode: mode, + Servers: servers, + Effective: effective, + DHCP: dhcp, + Info: info, + }) + log.Debugf("get dns config: mode=%s servers=%v effective=%v dhcp=%v info=%+v", mode, servers, effective, dhcp, info) +} + +func (s *Service) SetDNS(c *gin.Context) { + var req proto.SetDNSReq + var rsp proto.Response + + if err := proto.ParseFormRequest(c, &req); err != nil { + rsp.ErrRsp(c, -1, "invalid arguments") + return + } + + switch req.Mode { + case dnsModeManual: + if err := setManualDNS(req.Servers); err != nil { + log.Errorf("failed to set manual dns: %s", err) + rsp.ErrRsp(c, -2, err.Error()) + return + } + case dnsModeDHCP: + if err := setDHCPDNS(); err != nil { + log.Errorf("failed to set dhcp dns: %s", err) + rsp.ErrRsp(c, -3, err.Error()) + return + } + default: + rsp.ErrRsp(c, -1, "invalid dns mode") + return + } + + _ = exec.Command("sync").Run() + + rsp.OkRsp(c) + log.Debugf("set dns config: mode=%s servers=%v", req.Mode, req.Servers) +} + +func setManualDNS(servers []string) error { + normalized, err := validateDNSServers(servers) + if err != nil { + return err + } + if len(normalized) == 0 { + return fmt.Errorf("dns servers are required") + } + + if currentDNSMode() == dnsModeDHCP { + if err := refreshDHCPResolvCacheFromEffective(); err != nil { + log.Warnf("failed to refresh dhcp dns cache before switching to manual: %s", err) + } + } + + if err := os.MkdirAll(dnsConfigDir, 0o755); err != nil { + return fmt.Errorf("failed to create dns config directory: %w", err) + } + + if err := writeDNSMode(dnsModeManual); err != nil { + return err + } + + if err := os.WriteFile(dnsServersFile, []byte(strings.Join(normalized, "\n")+"\n"), 0o644); err != nil { + return fmt.Errorf("failed to write dns servers: %w", err) + } + + if err := renderResolvConf(bootResolvFile, normalized); err != nil { + return err + } + + if err := renderResolvConf(etcResolvFile, normalized); err != nil { + return err + } + + return installUDHCPCDNSHook() +} + +func setDHCPDNS() error { + dhcpConfig, err := readDHCPResolvConfig(currentDNSMode() == dnsModeDHCP) + if err != nil { + return fmt.Errorf("failed to read dhcp dns: %w", err) + } + if len(dhcpConfig.Servers) == 0 { + return fmt.Errorf("no dhcp dns is currently available") + } + + if err := os.MkdirAll(dnsConfigDir, 0o755); err != nil { + return fmt.Errorf("failed to create dns config directory: %w", err) + } + + if err := renderResolvConfig(dhcpResolvFile, dhcpConfig); err != nil { + return fmt.Errorf("failed to write dhcp dns cache: %w", err) + } + + if err := writeDNSMode(dnsModeDHCP); err != nil { + return err + } + + if err := preserveManualDNSServers(); err != nil { + return err + } + + if err := backupAndRemoveBootResolv(); err != nil { + return err + } + + if err := renderResolvConfig(etcResolvFile, dhcpConfig); err != nil { + return err + } + + return installUDHCPCDNSHook() +} + +func currentDNSMode() string { + mode := readDNSMode() + if mode == "" { + mode = defaultDNSMode() + } + + return mode +} + +func readDNSMode() string { + data, err := os.ReadFile(dnsModeFile) + if err != nil { + return "" + } + + mode := strings.TrimSpace(string(data)) + if mode != dnsModeManual && mode != dnsModeDHCP { + return "" + } + + return mode +} + +func defaultDNSMode() string { + if _, err := os.Stat(bootResolvFile); err == nil { + return dnsModeManual + } + + return dnsModeDHCP +} + +func writeDNSMode(mode string) error { + if err := os.WriteFile(dnsModeFile, []byte(mode+"\n"), 0o644); err != nil { + return fmt.Errorf("failed to write dns mode: %w", err) + } + + return nil +} + +func readManualDNSServers() []string { + servers, err := parsePlainDNSServers(dnsServersFile) + if err != nil { + return nil + } + + return servers +} + +func preserveManualDNSServers() error { + if len(readManualDNSServers()) > 0 { + return nil + } + + servers, _ := parseResolvConf(bootResolvFile) + if len(servers) == 0 { + servers, _ = parseResolvConf(bootResolvBackup) + } + if len(servers) == 0 { + return nil + } + + if err := os.WriteFile(dnsServersFile, []byte(strings.Join(servers, "\n")+"\n"), 0o644); err != nil { + return fmt.Errorf("failed to preserve manual dns servers: %w", err) + } + + return nil +} + +func parsePlainDNSServers(path string) ([]string, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + lines := strings.Split(string(data), "\n") + return normalizeDNSServers(lines) +} + +func readDHCPResolvConfig(allowEffectiveFallback bool) (resolvConfig, error) { + config, err := parseResolvConfig(dhcpResolvFile) + if err == nil && len(config.Servers) > 0 { + return config, nil + } + + if allowEffectiveFallback { + effective, effectiveErr := parseResolvConfig(etcResolvFile) + if effectiveErr == nil && len(effective.Servers) > 0 { + return effective, nil + } + } + + if err != nil && !os.IsNotExist(err) { + return resolvConfig{}, err + } + + return resolvConfig{}, nil +} + +func refreshDHCPResolvCacheFromEffective() error { + config, err := parseResolvConfig(etcResolvFile) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if len(config.Servers) == 0 { + return nil + } + if len(config.SearchDomains) == 0 { + cached, _ := parseResolvConfig(dhcpResolvFile) + config.SearchDomains = cached.SearchDomains + } + + return renderResolvConfig(dhcpResolvFile, config) +} + +func parseResolvConf(path string) ([]string, error) { + config, err := parseResolvConfig(path) + if err != nil { + return nil, err + } + + return config.Servers, nil +} + +func parseResolvConfig(path string) (resolvConfig, error) { + file, err := os.Open(path) + if err != nil { + return resolvConfig{}, err + } + defer func() { + _ = file.Close() + }() + + var servers []string + var searchDomains []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := stripInlineComment(scanner.Text()) + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + switch fields[0] { + case "nameserver": + servers = append(servers, fields[1]) + case "search": + searchDomains = append(searchDomains, fields[1:]...) + case "domain": + searchDomains = append(searchDomains, fields[1]) + } + } + + if err := scanner.Err(); err != nil { + return resolvConfig{}, err + } + + normalizedServers, err := normalizeDNSServers(servers) + if err != nil { + return resolvConfig{}, err + } + + return resolvConfig{ + Servers: normalizedServers, + SearchDomains: normalizeSearchDomains(searchDomains), + }, nil +} + +func getDNSInfo() proto.DNSInfo { + ifaceName, gateway := getDefaultIPv4Route() + if ifaceName == "" { + ifaceName = getFallbackIPv4Interface() + } + + info := proto.DNSInfo{ + Interface: ifaceName, + Gateway: gateway, + } + + dhcpConfig, _ := readDHCPResolvConfig(currentDNSMode() == dnsModeDHCP) + info.SearchDomains = dhcpConfig.SearchDomains + + if ifaceName == "" { + return info + } + + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return info + } + + info.Type = getDNSInterfaceType(iface.Name) + info.Address, info.SubnetMask = getIPv4AddressInfo(*iface) + + return info +} + +func getDefaultIPv4Route() (string, string) { + file, err := os.Open("/proc/net/route") + if err != nil { + return "", "" + } + defer func() { + _ = file.Close() + }() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) < 8 || fields[1] != "00000000" { + continue + } + + return fields[0], parseRouteGateway(fields[2]) + } + + return "", "" +} + +func parseRouteGateway(value string) string { + gateway, err := strconv.ParseUint(value, 16, 32) + if err != nil || gateway == 0 { + return "" + } + + return net.IPv4(byte(gateway), byte(gateway>>8), byte(gateway>>16), byte(gateway>>24)).String() +} + +func getFallbackIPv4Interface() string { + interfaces, err := net.Interfaces() + if err != nil { + return "" + } + + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + if getDNSInterfaceType(iface.Name) == "" { + continue + } + + address, _ := getIPv4AddressInfo(iface) + if address != "" { + return iface.Name + } + } + + return "" +} + +func getDNSInterfaceType(name string) string { + if strings.HasPrefix(name, "eth") || strings.HasPrefix(name, "en") { + return "Wired" + } + + if strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wl") { + return "Wireless" + } + + return "" +} + +func getIPv4AddressInfo(iface net.Interface) (string, string) { + addrs, err := iface.Addrs() + if err != nil { + return "", "" + } + + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok { + continue + } + + ip := ipNet.IP.To4() + if ip == nil { + continue + } + + ones, _ := ipNet.Mask.Size() + return fmt.Sprintf("%s/%d", ip.String(), ones), net.IP(ipNet.Mask).String() + } + + return "", "" +} + +func renderResolvConf(path string, servers []string) error { + return renderResolvConfig(path, resolvConfig{Servers: servers}) +} + +func renderResolvConfig(path string, config resolvConfig) error { + normalized, err := normalizeDNSServers(config.Servers) + if err != nil { + return err + } + if len(normalized) == 0 { + return fmt.Errorf("dns servers are required") + } + + var builder strings.Builder + searchDomains := normalizeSearchDomains(config.SearchDomains) + if len(searchDomains) > 0 { + builder.WriteString("search ") + builder.WriteString(strings.Join(searchDomains, " ")) + builder.WriteString("\n") + } + + for _, server := range normalized { + builder.WriteString("nameserver ") + builder.WriteString(server) + builder.WriteString("\n") + } + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return fmt.Errorf("failed to create dns file directory: %w", err) + } + + if err := os.WriteFile(path, []byte(builder.String()), 0o644); err != nil { + return fmt.Errorf("failed to write %s: %w", path, err) + } + + return nil +} + +func validateDNSServers(servers []string) ([]string, error) { + normalized, err := normalizeDNSServers(servers) + if err != nil { + return nil, err + } + if len(normalized) > maxDNSServers { + return nil, fmt.Errorf("too many dns servers") + } + + return normalized, nil +} + +func normalizeDNSServers(servers []string) ([]string, error) { + seen := make(map[string]struct{}) + var normalized []string + + for _, server := range servers { + server = normalizeDNSServer(server) + if server == "" { + continue + } + + ip := net.ParseIP(server) + if ip == nil { + return nil, fmt.Errorf("invalid dns server: %s", server) + } + + server = ip.String() + if _, exists := seen[server]; exists { + continue + } + + seen[server] = struct{}{} + normalized = append(normalized, server) + } + + return normalized, nil +} + +func normalizeSearchDomains(domains []string) []string { + seen := make(map[string]struct{}) + var normalized []string + + for _, domain := range domains { + domain = strings.TrimSpace(domain) + if domain == "" { + continue + } + if _, exists := seen[domain]; exists { + continue + } + + seen[domain] = struct{}{} + normalized = append(normalized, domain) + } + + return normalized +} + +func normalizeDNSServer(server string) string { + server = stripInlineComment(server) + server = strings.TrimSpace(server) + fields := strings.Fields(server) + if len(fields) == 0 { + return "" + } + + return fields[0] +} + +func stripInlineComment(value string) string { + if index := strings.Index(value, "#"); index >= 0 { + return value[:index] + } + + return value +} + +func backupAndRemoveBootResolv() error { + data, err := os.ReadFile(bootResolvFile) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("failed to read boot resolv: %w", err) + } + + if err := os.WriteFile(bootResolvBackup, data, 0o644); err != nil { + return fmt.Errorf("failed to backup boot resolv: %w", err) + } + + if err := os.Remove(bootResolvFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove boot resolv: %w", err) + } + + return nil +} + +func installUDHCPCDNSHook() error { + if err := os.MkdirAll(filepath.Dir(udhcpcHookFile), 0o755); err != nil { + return fmt.Errorf("failed to create udhcpc hook directory: %w", err) + } + + if err := os.WriteFile(udhcpcHookFile, []byte(udhcpcDNSHook), 0o755); err != nil { + return fmt.Errorf("failed to install udhcpc dns hook: %w", err) + } + + return nil +} diff --git a/server/service/network/scripts/99-nanokvm-dns b/server/service/network/scripts/99-nanokvm-dns new file mode 100644 index 0000000..f4748bc --- /dev/null +++ b/server/service/network/scripts/99-nanokvm-dns @@ -0,0 +1,56 @@ +#!/bin/sh + +ACTION="$1" +CACHE="/etc/resolv.conf.dhcp" +ACTIVE="/etc/resolv.conf" +MODE="/etc/kvm/network/dns.mode" +BOOT="/boot/resolv.conf" + +[ "$ACTION" = "bound" ] || [ "$ACTION" = "renew" ] || exit 0 +[ -n "$dns" ] || exit 0 + +TMPFILE="$(mktemp)" || exit 1 + +write_search_domain() { + if [ -n "$search" ]; then + printf "search" + for domain_name in $search; do + printf " %s" "$domain_name" + done + printf "\n" + return + fi + + if [ -n "$domain" ]; then + printf "domain %s\n" "$domain" + return + fi + + for source in "$CACHE" "$ACTIVE"; do + [ -f "$source" ] || continue + while IFS= read -r line; do + line="${line%%#*}" + set -- $line + [ "$1" = "search" ] || [ "$1" = "domain" ] || continue + [ -n "$2" ] || continue + printf "%s\n" "$line" + return + done < "$source" + done +} + +{ + write_search_domain + for server in $dns; do + printf "nameserver %s\n" "$server" + done +} > "$TMPFILE" + +cat "$TMPFILE" > "$CACHE" +MODE_VALUE="$(cat "$MODE" 2>/dev/null)" +if [ "$MODE_VALUE" = "dhcp" ] || { [ -z "$MODE_VALUE" ] && [ ! -f "$BOOT" ]; }; then + cat "$TMPFILE" > "$ACTIVE" +fi +rm -f "$TMPFILE" + +exit 0 diff --git a/web/src/api/network.ts b/web/src/api/network.ts index 3174652..00a4c38 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -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 }); +} diff --git a/web/src/components/ui/scroll-area.tsx b/web/src/components/ui/scroll-area.tsx index b1e79c4..82875a2 100644 --- a/web/src/components/ui/scroll-area.tsx +++ b/web/src/components/ui/scroll-area.tsx @@ -4,9 +4,12 @@ import clsx from 'clsx'; function ScrollArea({ className, + viewportRef, children, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + viewportRef?: React.Ref; +}) { return ( diff --git a/web/src/i18n/locales/ca.ts b/web/src/i18n/locales/ca.ts index acdd820..3e1f7b7 100644 --- a/web/src/i18n/locales/ca.ts +++ b/web/src/i18n/locales/ca.ts @@ -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: { diff --git a/web/src/i18n/locales/cz.ts b/web/src/i18n/locales/cz.ts index 05cfb99..c782a7f 100644 --- a/web/src/i18n/locales/cz.ts +++ b/web/src/i18n/locales/cz.ts @@ -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: { diff --git a/web/src/i18n/locales/da.ts b/web/src/i18n/locales/da.ts index 5617dab..1bd06be 100644 --- a/web/src/i18n/locales/da.ts +++ b/web/src/i18n/locales/da.ts @@ -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: { diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index c989cc3..6f42f7e 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -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: { diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 5fea0ba..ea1eab5 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -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: { diff --git a/web/src/i18n/locales/es.ts b/web/src/i18n/locales/es.ts index 4ed3c71..f281243 100644 --- a/web/src/i18n/locales/es.ts +++ b/web/src/i18n/locales/es.ts @@ -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: { diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index 36942af..c652450 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -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: { diff --git a/web/src/i18n/locales/hu.ts b/web/src/i18n/locales/hu.ts index eb65b15..c494df7 100644 --- a/web/src/i18n/locales/hu.ts +++ b/web/src/i18n/locales/hu.ts @@ -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: { diff --git a/web/src/i18n/locales/id.ts b/web/src/i18n/locales/id.ts index 800d18c..f4fc858 100644 --- a/web/src/i18n/locales/id.ts +++ b/web/src/i18n/locales/id.ts @@ -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: { diff --git a/web/src/i18n/locales/it.ts b/web/src/i18n/locales/it.ts index 4d78232..3f52595 100644 --- a/web/src/i18n/locales/it.ts +++ b/web/src/i18n/locales/it.ts @@ -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: { diff --git a/web/src/i18n/locales/ja.ts b/web/src/i18n/locales/ja.ts index 7fbc579..d4e77dd 100644 --- a/web/src/i18n/locales/ja.ts +++ b/web/src/i18n/locales/ja.ts @@ -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: { diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index 978911a..8c0468a 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -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: { diff --git a/web/src/i18n/locales/nb.ts b/web/src/i18n/locales/nb.ts index e9e86d0..936a84a 100644 --- a/web/src/i18n/locales/nb.ts +++ b/web/src/i18n/locales/nb.ts @@ -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: { diff --git a/web/src/i18n/locales/nl.ts b/web/src/i18n/locales/nl.ts index 8cf73e4..1b13b8a 100644 --- a/web/src/i18n/locales/nl.ts +++ b/web/src/i18n/locales/nl.ts @@ -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: { diff --git a/web/src/i18n/locales/pl.ts b/web/src/i18n/locales/pl.ts index 0674fc1..ceac4c4 100644 --- a/web/src/i18n/locales/pl.ts +++ b/web/src/i18n/locales/pl.ts @@ -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: { diff --git a/web/src/i18n/locales/pt_br.ts b/web/src/i18n/locales/pt_br.ts index 509170f..cbf79ad 100644 --- a/web/src/i18n/locales/pt_br.ts +++ b/web/src/i18n/locales/pt_br.ts @@ -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: { diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index 80d9673..5fa9eb8 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -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: { diff --git a/web/src/i18n/locales/se.ts b/web/src/i18n/locales/se.ts index 478086d..6c12327 100644 --- a/web/src/i18n/locales/se.ts +++ b/web/src/i18n/locales/se.ts @@ -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: { diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts index 5d12168..f98c06f 100644 --- a/web/src/i18n/locales/th.ts +++ b/web/src/i18n/locales/th.ts @@ -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: { diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index a93be17..da80b55 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -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: { diff --git a/web/src/i18n/locales/uk.ts b/web/src/i18n/locales/uk.ts index 4c4f331..68533a4 100644 --- a/web/src/i18n/locales/uk.ts +++ b/web/src/i18n/locales/uk.ts @@ -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: { diff --git a/web/src/i18n/locales/vi.ts b/web/src/i18n/locales/vi.ts index 7e36e70..d11f534 100644 --- a/web/src/i18n/locales/vi.ts +++ b/web/src/i18n/locales/vi.ts @@ -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: { diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index b734a2f..be9821f 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -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: { diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index 263714b..9bafc10 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -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: { diff --git a/web/src/pages/desktop/menu/settings/device/index.tsx b/web/src/pages/desktop/menu/settings/device/index.tsx index 4af84cc..c63e373 100644 --- a/web/src/pages/desktop/menu/settings/device/index.tsx +++ b/web/src/pages/desktop/menu/settings/device/index.tsx @@ -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 = () => {
- @@ -31,7 +28,6 @@ export const Device = () => { - diff --git a/web/src/pages/desktop/menu/settings/index.tsx b/web/src/pages/desktop/menu/settings/index.tsx index 8d59461..6981e73 100644 --- a/web/src/pages/desktop/menu/settings/index.tsx +++ b/web/src/pages/desktop/menu/settings/index.tsx @@ -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(null); const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom); @@ -42,6 +45,7 @@ export const Settings = () => { { id: 'about', icon: , component: }, { id: 'appearance', icon: , component: }, { id: 'device', icon: , component: }, + { id: 'network', icon: , component: }, { id: 'tailscale', icon: , @@ -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 = () => { ))}
- +
<>{tabs.find((tab) => tab.id === currentTab)?.component} diff --git a/web/src/pages/desktop/menu/settings/network/dns.tsx b/web/src/pages/desktop/menu/settings/network/dns.tsx new file mode 100644 index 0000000..399c732 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/network/dns.tsx @@ -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(); + 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 ( +
+
+
{title}
+ {description && ( +
{description}
+ )} +
+
{children}
+
+ ); +}; + +const InfoRow = ({ + label, + value, + isLast = false +}: { + label: string; + value?: string; + isLast?: boolean; +}) => { + return ( +
+
+ {label} + + {value || '-'} + +
+
+ ); +}; + +const ServerList = ({ servers }: { servers: string[] }) => { + const { t } = useTranslation(); + + if (!servers.length) { + return ( +
{t('settings.network.dns.none')}
+ ); + } + + return ( +
+ {servers.map((server, index) => ( +
+
+ {server} +
+
+ ))} +
+ ); +}; + +const EditableServerRow = ({ + value, + autoFocus, + onChange, + onRemove +}: { + value: string; + autoFocus: boolean; + onChange: (value: string) => void; + onRemove: () => void; +}) => { + const inputRef = useRef(null); + const normalized = normalizeServer(value); + const isInvalid = normalized !== '' && !isValidIP(normalized); + + useEffect(() => { + if (autoFocus && inputRef.current) { + inputRef.current.focus(); + } + }, [autoFocus]); + + return ( +
+
+ onChange(e.target.value)} + placeholder="0.0.0.0" + status={isInvalid ? 'error' : undefined} + /> +
+
+ ); +}; + +export const DNS = () => { + const { t } = useTranslation(); + + const [mode, setMode] = useState('dhcp'); + const [originalMode, setOriginalMode] = useState('dhcp'); + const [servers, setServers] = useState([]); + const [originalServers, setOriginalServers] = useState([]); + const [dhcp, setDHCP] = useState([]); + const [info, setInfo] = useState({}); + + 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 ( +
+ {/* Header row: title + segmented control */} +
+
+ {t('settings.network.dns.title')} + {t('settings.network.dns.description')} +
+ + { + setMode(val as DNSMode); + setMessage(''); + setError(''); + }} + options={[ + { label: t('settings.network.dns.dhcp'), value: 'dhcp' }, + { label: t('settings.network.dns.manual'), value: 'manual' } + ]} + /> +
+ +
+ + + + + + + + + {mode === 'manual' ? ( +
+ {servers.length === 0 ? ( +
+ {t('settings.network.dns.none')} +
+ ) : ( + servers.map((server, index) => ( + updateServer(index, val)} + onRemove={() => removeServer(index)} + /> + )) + )} + + {/* Add server button */} + {canAdd && ( +
+
+ +
+
+ )} + + {/* Validation hints */} + {(hasInvalidServer || isExceedMax) && ( +
+ {hasInvalidServer && ( +
{t('settings.network.dns.invalid')}
+ )} + {isExceedMax && ( +
+ {t('settings.network.dns.maxServers', { count: maxServers })} +
+ )} +
+ )} +
+ ) : ( + + )} +
+
+ + {/* Footer: status + save button */} + {(hasChanges || statusText) && ( +
+ {statusText} + + +
+ )} +
+ ); +}; diff --git a/web/src/pages/desktop/menu/settings/network/index.tsx b/web/src/pages/desktop/menu/settings/network/index.tsx new file mode 100644 index 0000000..a85fd1c --- /dev/null +++ b/web/src/pages/desktop/menu/settings/network/index.tsx @@ -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 ( + <> +
{t('settings.network.title')}
+ + +
+ + +
+ + + + + + ); +}; diff --git a/web/src/pages/desktop/menu/settings/device/tls.tsx b/web/src/pages/desktop/menu/settings/network/tls.tsx similarity index 95% rename from web/src/pages/desktop/menu/settings/device/tls.tsx rename to web/src/pages/desktop/menu/settings/network/tls.tsx index 96f0ae8..2b29ce6 100644 --- a/web/src/pages/desktop/menu/settings/device/tls.tsx +++ b/web/src/pages/desktop/menu/settings/network/tls.tsx @@ -52,7 +52,7 @@ export const Tls = () => { HTTPS {
- {t('settings.device.tls.description')} + {t('settings.network.tls.description')}
diff --git a/web/src/pages/desktop/menu/settings/device/wifi.tsx b/web/src/pages/desktop/menu/settings/network/wifi.tsx similarity index 86% rename from web/src/pages/desktop/menu/settings/device/wifi.tsx rename to web/src/pages/desktop/menu/settings/network/wifi.tsx index a076463..d6114a6 100644 --- a/web/src/pages/desktop/menu/settings/device/wifi.tsx +++ b/web/src/pages/desktop/menu/settings/network/wifi.tsx @@ -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 (
-
- {t('settings.device.wifi.title')} - {t('settings.device.wifi.apMode')} +
+ {t('settings.network.wifi.title')} + {t('settings.network.wifi.apMode')}