Compare commits

..

1 Commits

Author SHA1 Message Date
Aveline 9aa9b44fbe
Fix localization issues in zh.json 2025-10-07 23:14:03 +02:00
11 changed files with 114 additions and 192 deletions

View File

@ -6,10 +6,6 @@
"jetkvm_logo": "JetKVM Logo",
"load": "Load",
"unknown_error": "Unknown error",
"close": "Close",
"cancel": "Cancel",
"action_bar_virtual_media": "Virtual Media",
"action_bar_paste_text": "Paste text",
"action_bar_web_terminal": "Web Terminal",
@ -20,7 +16,6 @@
"action_bar_settings": "Settings",
"action_bar_fullscreen": "Fullscreen",
"action_bar_exit_fullscreen": "Exit Fullscreen",
"extensions_popover_extensions": "Extensions",
"extension_popover_set_error_notification": "Failed to set active extension: {error}",
"extension_popover_unload_extension": "Unload Extension",
@ -31,7 +26,6 @@
"extensions_dc_power_control_description": "Control your DC Power extension",
"extension_serial_console": "Serial Console",
"extension_serial_console_description": "Access your serial console extension",
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
"atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
"atx_power_control_power_button": "Power",
@ -40,7 +34,6 @@
"atx_power_control_reset_button": "Reset",
"atx_power_control_power_led": "Power LED",
"atx_power_control_hdd_led": "HDD LED",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
"dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}",
"dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
@ -49,14 +42,12 @@
"dc_power_control_restore_power_state": "Restore Power Loss",
"dc_power_control_power_on_state": "Power ON",
"dc_power_control_power_off_state": "Power OFF",
"dc_power_control_restore_last_state": "Last State",
"dc_power_control_voltage": "Voltage",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Current",
"dc_power_control_current_unit": "A",
"dc_power_control_power": "Power",
"dc_power_control_power_unit": "W",
"serial_console_get_settings_error": "Failed to get serial console settings: {error}",
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
"serial_console_configure_description": "Configure your serial console settings",
@ -69,76 +60,5 @@
"serial_console_parity_odd": "Odd Parity",
"serial_console_parity_none": "No Parity",
"serial_console_parity_mark": "Mark Parity",
"serial_console_parity_space": "Space Parity",
"wake_on_lan_add_device_device_name": "Device Name",
"wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC Address",
"wake_on_lan_add_device_back": "Back",
"wake_on_lan_add_device_save_device": "Save Device",
"paste_modal_paste_text": "Paste text",
"paste_modal_paste_text_description": "Paste text from your client to the remote host",
"paste_modal_paste_from_host": "Paste from host",
"paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
"paste_modal_delay_between_keys": "Delay between keys",
"paste_modal_delay_out_of_range": "Delay must be between {min} and {max}",
"paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
"paste_modal_confirm_paste": "Confirm Paste",
"mount_virtual_media": "Virtual Media",
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
"mount_no_mounted_media": "No mounted media",
"mount_add_file_to_get_started": "Add a file to get started",
"mount_streaming_from_url": "Streaming from URL",
"mount_mounted_from_storage": "Mounted from JetKVM Storage",
"mount_unmount": "Unmount",
"mount_add_new_media": "Add New Media",
"mount_get_state_error": "Failed to get virtual media state: {error}",
"mount_unmount_error": "Failed to unmount image: {error}",
"mount_mounted_as": "Mounted as",
"mount_mode_disk": "Disk",
"mount_mode_cdrom": "CD-ROM",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_description": "Send a Magic Packet to wake up a remote device.",
"wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"wake_on_lan_failed_add_device": "Failed to add device",
"wake_on_lan_empty_no_devices_added": "No devices added",
"wake_on_lan_empty_add_device_to_start": "Add a device to start using Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Add New Device",
"wake_on_lan_device_list_wake": "Wake",
"wake_on_lan_device_list_delete_device": "Delete device",
"wake_on_lan_device_list_add_new_device": "Add New Device",
"connection_stats_sidebar": "Connection Stats",
"connection_stats_connection": "Connection",
"connection_stats_connection_description": "The connection between the client and the JetKVM.",
"connection_stats_round_trip_time": "Round-Trip Time",
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
"connection_stats_video": "Video",
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
"connection_stats_network_stability": "Network Stability",
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_playback_delay": "Playback Delay",
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
"connection_stats_packets_lost": "Packets Lost",
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
"connection_stats_frames_per_second": "Frames per second",
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second."
"serial_console_parity_space": "Space Parity"
}

View File

@ -11,7 +11,7 @@
"action_bar_web_terminal": "网页终端",
"action_bar_wake_on_lan": "局域网唤醒",
"action_bar_virtual_keyboard": "虚拟键盘",
"action_bar_extension": "扩",
"action_bar_extension": "扩",
"action_bar_connection_stats": "连接统计",
"action_bar_settings": "设置",
"action_bar_fullscreen": "全屏",
@ -28,7 +28,7 @@
"extension_serial_console_description": "访问串行控制台扩展",
"atx_power_control_get_state_error": "无法获取 ATX 电源状态:{error}",
"atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}",
"atx_power_control_power_button": "力量",
"atx_power_control_power_button": "电源",
"atx_power_control_short_power_button": "短按",
"atx_power_control_long_power_button": "长按",
"atx_power_control_reset_button": "重置",
@ -43,11 +43,11 @@
"dc_power_control_power_on_state": "开启电源",
"dc_power_control_power_off_state": "关闭电源",
"dc_power_control_voltage": "电压",
"dc_power_control_voltage_unit": "",
"dc_power_control_current": "安培",
"dc_power_control_current_unit": "一个",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "电流",
"dc_power_control_current_unit": "A",
"dc_power_control_power": "瓦特",
"dc_power_control_power_unit": "西",
"dc_power_control_power_unit": "W",
"serial_console_get_state_error": "无法获取串行控制台设置: {error}",
"serial_console_set_power_state_error": "无法将串行控制台设置设置为 {settings} : {error}",
"serial_console_configure_description": "配置串行控制台设置",
@ -55,12 +55,12 @@
"serial_console_baud_rate": "波特率",
"serial_console_data_bits": "数据位",
"serial_console_stop_bits": "停止位",
"serial_console_parity": "平价",
"serial_console_parity": "奇偶校验位",
"serial_console_parity_even": "偶校验",
"serial_console_parity_odd": "奇校验",
"serial_console_parity_none": "无奇偶校验",
"serial_console_parity_mark": "马克·帕里蒂",
"serial_console_parity_space": "空间平价",
"serial_console_parity_none": "无",
"serial_console_parity_mark": "Mark",
"serial_console_parity_space": "Space",
"serial_console_get_settings_error": "无法获取串行控制台设置: {error}",
"serial_console_set_settings_error": "无法将串行控制台设置设置为 {settings} : {error}"
}

View File

@ -103,7 +103,7 @@ export function DCPowerControl() {
options={[
{ value: '0', label: m.dc_power_control_power_off_state()},
{ value: '1', label: m.dc_power_control_power_on_state()},
{ value: '2', label: m.dc_power_control_restore_last_state()},
{ value: '2', label: m.dc_power_control_restore_power_state() },
]}
/>
</div>

View File

@ -1,17 +1,20 @@
import { PlusCircleIcon } from "@heroicons/react/20/solid";
import { forwardRef, useEffect, useCallback } from "react";
import { LuLink, LuPlus, LuRadioReceiver } from "react-icons/lu";
import {
LuLink,
LuPlus,
LuRadioReceiver,
} from "react-icons/lu";
import { useClose } from "@headlessui/react";
import { useLocation } from "react-router";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button";
import Card, { GridCard } from "@components/Card";
import { formatters } from "@/utils";
import { RemoteVirtualMediaState, useMountMediaStore } from "@hooks/stores";
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import notifications from "@/notifications";
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
@ -22,7 +25,9 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const syncRemoteVirtualMediaState = useCallback(() => {
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
if ("error" in response) {
notifications.error(m.mount_get_state_error({ error: response.error.message }));
notifications.error(
`Failed to get virtual media state: ${response.error.message}`,
);
} else {
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
}
@ -32,7 +37,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const handleUnmount = () => {
send("unmountImage", {}, (response: JsonRpcResponse) => {
if ("error" in response) {
notifications.error(m.mount_unmount_error({ error: response.error.message }));
notifications.error(`Failed to unmount image: ${response.error.message}`);
} else {
syncRemoteVirtualMediaState();
}
@ -52,10 +57,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</div>
<div className="space-y-1">
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
{m.mount_no_mounted_media()}
No mounted media
</h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
{m.mount_add_file_to_get_started()}
Add a file to get started
</p>
</div>
</div>
@ -76,7 +81,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card>
</div>
<h3 className="text-base font-semibold text-black dark:text-white">
{m.mount_streaming_from_url()}
Streaming from URL
</h3>
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(url, 55)}
@ -100,7 +105,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card>
</div>
<h3 className="text-base font-semibold text-black dark:text-white">
{m.mount_mounted_from_storage()}
Mounted from JetKVM Storage
</h3>
<p className="text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(path, 50)}
@ -133,8 +138,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<div className="h-full space-y-4">
<div className="space-y-4">
<SettingsPageHeader
title={m.mount_virtual_media()}
description={m.mount_virtual_media_description()}
title="Virtual Media"
description="Mount an image to boot from or install an operating system."
/>
<div
@ -158,9 +163,9 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
{remoteVirtualMediaState ? (
<div className="flex select-none items-center justify-between text-xs">
<div className="select-none text-white dark:text-slate-300">
<span>{m.mount_mounted_as()}</span>{" "}
<span>Mounted as</span>{" "}
<span className="font-semibold">
{remoteVirtualMediaState.mode === "Disk" ? m.mount_mode_disk() : m.mount_mode_cdrom()}
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
</span>
</div>
@ -168,7 +173,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="blank"
text={m.close()}
text="Close"
onClick={() => {
close();
}}
@ -176,7 +181,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="light"
text={m.mount_unmount()}
text="Unmount"
LeadingIcon={({ className }) => (
<svg
className={`${className} h-2.5 w-2.5 shrink-0`}
@ -222,7 +227,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="blank"
text={m.close()}
text="Close"
onClick={() => {
close();
}}
@ -230,7 +235,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="primary"
text={m.mount_add_new_media()}
text="Add New Media"
onClick={() => {
setModalView("mode");
navigateTo("/mount");

View File

@ -1,14 +1,13 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useClose } from "@headlessui/react";
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LuCornerDownLeft } from "react-icons/lu";
import { cx } from "@/cva.config";
import { m } from "@localizations/messages.js";
import { useHidStore, useSettingsStore, useUiStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import useKeyboard, { type MacroStep } from "@hooks/useKeyboard";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import notifications from "@/notifications";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
@ -123,8 +122,8 @@ export default function PasteModal() {
<div className="h-full space-y-4">
<div className="space-y-4">
<SettingsPageHeader
title={m.paste_modal_paste_text()}
description={m.paste_modal_paste_text_description()}
title="Paste text"
description="Paste text from your client to the remote host"
/>
<div
@ -144,7 +143,7 @@ export default function PasteModal() {
>
<TextAreaWithLabel
ref={TextAreaRef}
label={m.paste_modal_paste_from_host()}
label="Paste from host"
rows={4}
onKeyUp={e => e.stopPropagation()}
maxLength={pasteMaxLength}
@ -177,7 +176,7 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400">
{m.paste_modal_invalid_chars_intro()}{" "}
The following characters won&apos;t be pasted:{" "}
{invalidChars.join(", ")}
</span>
</div>
@ -187,8 +186,8 @@ export default function PasteModal() {
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
<InputFieldWithLabel
type="number"
label={m.paste_modal_delay_between_keys()}
placeholder={m.paste_modal_delay_between_keys()}
label="Delay between keys"
placeholder="Delay between keys"
min={50}
max={65534}
value={delayValue}
@ -200,14 +199,15 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400">
{m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
Delay must be between 50 and 65534
</span>
</div>
)}
</div>
<div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400">
{m.paste_modal_sending_using_layout({ iso: selectedKeyboard.isoCode, name: selectedKeyboard.name })}
Sending text using keyboard layout: {selectedKeyboard.isoCode}-
{selectedKeyboard.name}
</p>
</div>
</div>
@ -224,7 +224,7 @@ export default function PasteModal() {
<Button
size="SM"
theme="blank"
text={m.cancel()}
text="Cancel"
onClick={() => {
onCancelPasteMode();
close();
@ -233,7 +233,7 @@ export default function PasteModal() {
<Button
size="SM"
theme="primary"
text={m.paste_modal_confirm_paste()}
text="Confirm Paste"
disabled={isPasteInProgress}
onClick={onConfirmPaste}
LeadingIcon={LuCornerDownLeft}

View File

@ -1,9 +1,8 @@
import { useState, useRef } from "react";
import { LuPlus, LuArrowLeft } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { InputFieldWithLabel } from "@components/InputField";
import { Button } from "@components/Button";
import { InputFieldWithLabel } from "@/components/InputField";
import { Button } from "@/components/Button";
interface AddDeviceFormProps {
onAddDevice: (name: string, macAddress: string) => void;
@ -35,8 +34,8 @@ export default function AddDeviceForm({
>
<InputFieldWithLabel
ref={nameInputRef}
placeholder={m.wake_on_lan_add_device_example_device_name()}
label={m.wake_on_lan_add_device_device_name()}
placeholder="Plex Media Server"
label="Device Name"
required
onChange={e => {
setIsDeviceNameValid(e.target.validity.valid);
@ -47,7 +46,7 @@ export default function AddDeviceForm({
<InputFieldWithLabel
ref={macInputRef}
placeholder="00:b0:d0:63:c2:26"
label={m.wake_on_lan_add_device_mac_address()}
label="MAC Address"
onKeyUp={e => e.stopPropagation()}
required
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
@ -83,14 +82,14 @@ export default function AddDeviceForm({
<Button
size="SM"
theme="light"
text={m.wake_on_lan_add_device_back()}
text="Back"
LeadingIcon={LuArrowLeft}
onClick={() => setShowAddForm(false)}
/>
<Button
size="SM"
theme="primary"
text={m.wake_on_lan_add_device_save_device()}
text="Save Device"
disabled={!isDeviceNameValid || !isMacAddressValid}
onClick={() => {
const deviceName = nameInputRef.current?.value || "";

View File

@ -1,9 +1,8 @@
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button";
import Card from "@components/Card";
import { FieldError } from "@components/InputField";
import { Button } from "@/components/Button";
import Card from "@/components/Card";
import { FieldError } from "@/components/InputField";
export interface StoredDevice {
name: string;
@ -47,7 +46,7 @@ export default function DeviceList({
<Button
size="XS"
theme="light"
text={m.wake_on_lan_device_list_wake()}
text="Wake"
LeadingIcon={LuSend}
onClick={() => onSendMagicPacket(device.macAddress)}
/>
@ -56,7 +55,7 @@ export default function DeviceList({
theme="danger"
LeadingIcon={LuTrash2}
onClick={() => onDeleteDevice(index)}
aria-label={m.wake_on_lan_device_list_delete_device()}
aria-label="Delete device"
/>
</div>
</div>
@ -70,11 +69,11 @@ export default function DeviceList({
animationDelay: "0.2s",
}}
>
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
<Button
size="SM"
theme="primary"
text={m.wake_on_lan_device_list_add_new_device()}
text="Add New Device"
onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus}
/>

View File

@ -1,9 +1,8 @@
import { PlusCircleIcon } from "@heroicons/react/16/solid";
import { LuPlus } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import Card from "@components/Card";
import { Button } from "@components/Button";
import Card from "@/components/Card";
import { Button } from "@/components/Button";
export default function EmptyStateCard({
onCancelWakeOnLanModal,
@ -26,10 +25,10 @@ export default function EmptyStateCard({
</Card>
</div>
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
{m.wake_on_lan_empty_no_devices_added()}
No devices added
</h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
{m.wake_on_lan_empty_add_device_to_start()}
Add a device to start using Wake-on-LAN
</p>
</div>
</div>
@ -42,11 +41,11 @@ export default function EmptyStateCard({
animationDelay: "0.2s",
}}
>
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
<Button
size="SM"
theme="primary"
text={m.wake_on_lan_empty_add_new_device()}
text="Add New Device"
onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus}
/>

View File

@ -1,11 +1,10 @@
import { useCallback, useEffect, useState } from "react";
import { useClose } from "@headlessui/react";
import { m } from "@localizations/messages.js";
import { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useRTCStore, useUiStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useRTCStore, useUiStore } from "@/hooks/stores";
import notifications from "@/notifications";
import EmptyStateCard from "./EmptyStateCard";
@ -36,12 +35,12 @@ export default function WakeOnLanModal() {
if ("error" in resp) {
const isInvalid = resp.error.data?.includes("invalid MAC address");
if (isInvalid) {
setErrorMessage(m.wake_on_lan_invalid_mac());
setErrorMessage("Invalid MAC address");
} else {
setErrorMessage(m.wake_on_lan_failed_send_magic());
setErrorMessage("Failed to send Magic Packet");
}
} else {
notifications.success(m.wake_on_lan_magic_sent_success());
notifications.success("Magic Packet sent successfully");
setDisableVideoFocusTrap(false);
close();
}
@ -88,7 +87,7 @@ export default function WakeOnLanModal() {
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error("Failed to add Wake-on-LAN device:", resp.error);
setAddDeviceErrorMessage(m.wake_on_lan_failed_add_device());
setAddDeviceErrorMessage("Failed to add device");
} else {
setShowAddForm(false);
syncStoredDevices();
@ -104,8 +103,8 @@ export default function WakeOnLanModal() {
<div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="space-y-4">
<SettingsPageHeader
title={m.wake_on_lan()}
description={m.wake_on_lan_description()}
title="Wake On LAN"
description="Send a Magic Packet to wake up a remote device."
/>
{showAddForm ? (

View File

@ -1,12 +1,12 @@
import { useInterval } from "usehooks-ts";
import { m } from "@localizations/messages.js";
import { createChartArray, Metric } from "@components/Metric";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import SidebarHeader from "@components/SidebarHeader";
import { useRTCStore, useUiStore } from "@hooks/stores";
import SidebarHeader from "@/components/SidebarHeader";
import { useRTCStore, useUiStore } from "@/hooks/stores";
import { someIterable } from "@/utils";
import { createChartArray, Metric } from "../Metric";
import { SettingsSectionHeader } from "../SettingsSectionHeader";
export default function ConnectionStatsSidebar() {
const { sidebarView, setSidebarView } = useUiStore();
const {
@ -95,7 +95,7 @@ export default function ConnectionStatsSidebar() {
return (
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
<SidebarHeader title={m.connection_stats_sidebar()} setSidebarView={setSidebarView} />
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
<div className="space-y-4">
{sidebarView === "connection-stats" && (
@ -103,12 +103,12 @@ export default function ConnectionStatsSidebar() {
{/* Connection Group */}
<div className="space-y-3">
<SettingsSectionHeader
title={m.connection_stats_connection()}
description={m.connection_stats_connection_description()}
title="Connection"
description="The connection between the client and the JetKVM."
/>
<Metric
title={m.connection_stats_round_trip_time()}
description={m.connection_stats_round_trip_time_description()}
title="Round-Trip Time"
description="Round-trip time for the active ICE candidate pair between peers."
stream={iceCandidatePairStats}
metric="currentRoundTripTime"
map={x => ({
@ -123,16 +123,16 @@ export default function ConnectionStatsSidebar() {
{/* Video Group */}
<div className="space-y-3">
<SettingsSectionHeader
title={m.connection_stats_video()}
description={m.connection_stats_video_description()}
title="Video"
description="The video stream from the JetKVM to the client."
/>
{/* RTP Jitter */}
<Metric
title={m.connection_stats_network_stability()}
badge={m.connection_stats_badge_jitter()}
title="Network Stability"
badge="Jitter"
badgeTheme="light"
description={m.connection_stats_network_stability_description()}
description="How steady the flow of inbound video packets is across the network."
stream={inboundVideoRtpStats}
metric="jitter"
map={x => ({
@ -145,9 +145,9 @@ export default function ConnectionStatsSidebar() {
{/* Playback Delay */}
<Metric
title={m.connection_stats_playback_delay()}
description={m.connection_stats_playback_delay_description()}
badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
title="Playback Delay"
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly."
badge="Jitter Buffer Avg. Delay"
badgeTheme="light"
data={jitterBufferAvgDelayData}
gate={inboundVideoRtpStats}
@ -167,8 +167,8 @@ export default function ConnectionStatsSidebar() {
{/* Packets Lost */}
<Metric
title={m.connection_stats_packets_lost()}
description={m.connection_stats_packets_lost_description()}
title="Packets Lost"
description="Count of lost inbound video RTP packets."
stream={inboundVideoRtpStats}
metric="packetsLost"
domain={[0, 100]}
@ -177,8 +177,8 @@ export default function ConnectionStatsSidebar() {
{/* Frames Per Second */}
<Metric
title={m.connection_stats_frames_per_second()}
description={m.connection_stats_frames_per_second_description()}
title="Frames per second"
description="Number of inbound video frames displayed per second."
stream={inboundVideoRtpStats}
metric="framesPerSecond"
domain={[0, 80]}

View File

@ -4,6 +4,7 @@ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
import Card from "@/components/Card";
interface NotificationOptions {
duration?: number;
// Add other options as needed
@ -33,7 +34,7 @@ const ToastContent = ({
const notifications = {
success: (message: string, options?: NotificationOptions) => {
return toast.custom(
(t: Toast) => (
t => (
<ToastContent
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
message={message}
@ -46,7 +47,7 @@ const notifications = {
error: (message: string, options?: NotificationOptions) => {
return toast.custom(
(t: Toast) => (
t => (
<ToastContent
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
message={message}
@ -63,9 +64,9 @@ function useMaxToasts(max: number) {
useEffect(() => {
toasts
.filter((t: Toast) => t.visible) // Only consider visible toasts
.filter((_: Toast, i: number) => i >= max) // Is toast index over limit?
.forEach((t: Toast) => toast.dismiss(t.id)); // Dismiss Use toast.remove(t.id) for no exit animation
.filter(t => t.visible) // Only consider visible toasts
.filter((_, i) => i >= max) // Is toast index over limit?
.forEach(t => toast.dismiss(t.id)); // Dismiss Use toast.remove(t.id) for no exit animation
}, [toasts, max]);
}