mirror of https://github.com/jetkvm/kvm.git
Compare commits
3 Commits
eaf582e1cf
...
ca4c1b393d
| Author | SHA1 | Date |
|---|---|---|
|
|
ca4c1b393d | |
|
|
985b53c02b | |
|
|
474cb70e80 |
|
|
@ -6,6 +6,10 @@
|
||||||
"jetkvm_logo": "JetKVM Logo",
|
"jetkvm_logo": "JetKVM Logo",
|
||||||
"load": "Load",
|
"load": "Load",
|
||||||
"unknown_error": "Unknown error",
|
"unknown_error": "Unknown error",
|
||||||
|
|
||||||
|
"close": "Close",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
|
||||||
"action_bar_virtual_media": "Virtual Media",
|
"action_bar_virtual_media": "Virtual Media",
|
||||||
"action_bar_paste_text": "Paste text",
|
"action_bar_paste_text": "Paste text",
|
||||||
"action_bar_web_terminal": "Web Terminal",
|
"action_bar_web_terminal": "Web Terminal",
|
||||||
|
|
@ -16,6 +20,7 @@
|
||||||
"action_bar_settings": "Settings",
|
"action_bar_settings": "Settings",
|
||||||
"action_bar_fullscreen": "Fullscreen",
|
"action_bar_fullscreen": "Fullscreen",
|
||||||
"action_bar_exit_fullscreen": "Exit Fullscreen",
|
"action_bar_exit_fullscreen": "Exit Fullscreen",
|
||||||
|
|
||||||
"extensions_popover_extensions": "Extensions",
|
"extensions_popover_extensions": "Extensions",
|
||||||
"extension_popover_set_error_notification": "Failed to set active extension: {error}",
|
"extension_popover_set_error_notification": "Failed to set active extension: {error}",
|
||||||
"extension_popover_unload_extension": "Unload Extension",
|
"extension_popover_unload_extension": "Unload Extension",
|
||||||
|
|
@ -26,6 +31,7 @@
|
||||||
"extensions_dc_power_control_description": "Control your DC Power extension",
|
"extensions_dc_power_control_description": "Control your DC Power extension",
|
||||||
"extension_serial_console": "Serial Console",
|
"extension_serial_console": "Serial Console",
|
||||||
"extension_serial_console_description": "Access your serial console extension",
|
"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_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_send_action_error": "Failed to send ATX power action {action}: {error}",
|
||||||
"atx_power_control_power_button": "Power",
|
"atx_power_control_power_button": "Power",
|
||||||
|
|
@ -34,6 +40,7 @@
|
||||||
"atx_power_control_reset_button": "Reset",
|
"atx_power_control_reset_button": "Reset",
|
||||||
"atx_power_control_power_led": "Power LED",
|
"atx_power_control_power_led": "Power LED",
|
||||||
"atx_power_control_hdd_led": "HDD LED",
|
"atx_power_control_hdd_led": "HDD LED",
|
||||||
|
|
||||||
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
|
"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_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}",
|
"dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
|
||||||
|
|
@ -42,12 +49,14 @@
|
||||||
"dc_power_control_restore_power_state": "Restore Power Loss",
|
"dc_power_control_restore_power_state": "Restore Power Loss",
|
||||||
"dc_power_control_power_on_state": "Power ON",
|
"dc_power_control_power_on_state": "Power ON",
|
||||||
"dc_power_control_power_off_state": "Power OFF",
|
"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": "Voltage",
|
||||||
"dc_power_control_voltage_unit": "V",
|
"dc_power_control_voltage_unit": "V",
|
||||||
"dc_power_control_current": "Current",
|
"dc_power_control_current": "Current",
|
||||||
"dc_power_control_current_unit": "A",
|
"dc_power_control_current_unit": "A",
|
||||||
"dc_power_control_power": "Power",
|
"dc_power_control_power": "Power",
|
||||||
"dc_power_control_power_unit": "W",
|
"dc_power_control_power_unit": "W",
|
||||||
|
|
||||||
"serial_console_get_settings_error": "Failed to get serial console settings: {error}",
|
"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_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
|
||||||
"serial_console_configure_description": "Configure your serial console settings",
|
"serial_console_configure_description": "Configure your serial console settings",
|
||||||
|
|
@ -60,5 +69,76 @@
|
||||||
"serial_console_parity_odd": "Odd Parity",
|
"serial_console_parity_odd": "Odd Parity",
|
||||||
"serial_console_parity_none": "No Parity",
|
"serial_console_parity_none": "No Parity",
|
||||||
"serial_console_parity_mark": "Mark Parity",
|
"serial_console_parity_mark": "Mark Parity",
|
||||||
"serial_console_parity_space": "Space 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."
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ export function DCPowerControl() {
|
||||||
options={[
|
options={[
|
||||||
{ value: '0', label: m.dc_power_control_power_off_state()},
|
{ value: '0', label: m.dc_power_control_power_off_state()},
|
||||||
{ value: '1', label: m.dc_power_control_power_on_state()},
|
{ value: '1', label: m.dc_power_control_power_on_state()},
|
||||||
{ value: '2', label: m.dc_power_control_restore_power_state() },
|
{ value: '2', label: m.dc_power_control_restore_last_state()},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||||
import { forwardRef, useEffect, useCallback } from "react";
|
import { forwardRef, useEffect, useCallback } from "react";
|
||||||
import {
|
import { LuLink, LuPlus, LuRadioReceiver } from "react-icons/lu";
|
||||||
LuLink,
|
|
||||||
LuPlus,
|
|
||||||
LuRadioReceiver,
|
|
||||||
} from "react-icons/lu";
|
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { formatters } from "@/utils";
|
import { formatters } from "@/utils";
|
||||||
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
import { RemoteVirtualMediaState, useMountMediaStore } from "@hooks/stores";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
|
|
@ -25,9 +22,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const syncRemoteVirtualMediaState = useCallback(() => {
|
const syncRemoteVirtualMediaState = useCallback(() => {
|
||||||
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
||||||
if ("error" in response) {
|
if ("error" in response) {
|
||||||
notifications.error(
|
notifications.error(m.mount_get_state_error({ error: response.error.message }));
|
||||||
`Failed to get virtual media state: ${response.error.message}`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
|
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +32,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const handleUnmount = () => {
|
const handleUnmount = () => {
|
||||||
send("unmountImage", {}, (response: JsonRpcResponse) => {
|
send("unmountImage", {}, (response: JsonRpcResponse) => {
|
||||||
if ("error" in response) {
|
if ("error" in response) {
|
||||||
notifications.error(`Failed to unmount image: ${response.error.message}`);
|
notifications.error(m.mount_unmount_error({ error: response.error.message }));
|
||||||
} else {
|
} else {
|
||||||
syncRemoteVirtualMediaState();
|
syncRemoteVirtualMediaState();
|
||||||
}
|
}
|
||||||
|
|
@ -57,10 +52,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||||
No mounted media
|
{m.mount_no_mounted_media()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||||
Add a file to get started
|
{m.mount_add_file_to_get_started()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,7 +76,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||||
Streaming from URL
|
{m.mount_streaming_from_url()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
|
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
|
||||||
{formatters.truncateMiddle(url, 55)}
|
{formatters.truncateMiddle(url, 55)}
|
||||||
|
|
@ -105,7 +100,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
<h3 className="text-base font-semibold text-black dark:text-white">
|
||||||
Mounted from JetKVM Storage
|
{m.mount_mounted_from_storage()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-slate-900 dark:text-slate-100">
|
<p className="text-sm text-slate-900 dark:text-slate-100">
|
||||||
{formatters.truncateMiddle(path, 50)}
|
{formatters.truncateMiddle(path, 50)}
|
||||||
|
|
@ -138,8 +133,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<div className="h-full space-y-4">
|
<div className="h-full space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Virtual Media"
|
title={m.mount_virtual_media()}
|
||||||
description="Mount an image to boot from or install an operating system."
|
description={m.mount_virtual_media_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -162,10 +157,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
</div>
|
</div>
|
||||||
{remoteVirtualMediaState ? (
|
{remoteVirtualMediaState ? (
|
||||||
<div className="flex select-none items-center justify-between text-xs">
|
<div className="flex select-none items-center justify-between text-xs">
|
||||||
<div className="select-none text-white dark:text-slate-300">
|
<div className="select-none text-white dark:text-slate-300">
|
||||||
<span>Mounted as</span>{" "}
|
<span>{m.mount_mounted_as()}</span>{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
|
{remoteVirtualMediaState.mode === "Disk" ? m.mount_mode_disk() : m.mount_mode_cdrom()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -173,7 +168,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text="Close"
|
text={m.close()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
|
|
@ -181,7 +176,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Unmount"
|
text={m.mount_unmount()}
|
||||||
LeadingIcon={({ className }) => (
|
LeadingIcon={({ className }) => (
|
||||||
<svg
|
<svg
|
||||||
className={`${className} h-2.5 w-2.5 shrink-0`}
|
className={`${className} h-2.5 w-2.5 shrink-0`}
|
||||||
|
|
@ -227,7 +222,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text="Close"
|
text={m.close()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
|
|
@ -235,7 +230,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Add New Media"
|
text={m.mount_add_new_media()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalView("mode");
|
setModalView("mode");
|
||||||
navigateTo("/mount");
|
navigateTo("/mount");
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import { LuCornerDownLeft } from "react-icons/lu";
|
import { LuCornerDownLeft } from "react-icons/lu";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
import { m } from "@localizations/messages.js";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useHidStore, useSettingsStore, useUiStore } from "@hooks/stores";
|
||||||
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
import useKeyboard, { type MacroStep } from "@hooks/useKeyboard";
|
||||||
|
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
|
|
@ -122,8 +123,8 @@ export default function PasteModal() {
|
||||||
<div className="h-full space-y-4">
|
<div className="h-full space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Paste text"
|
title={m.paste_modal_paste_text()}
|
||||||
description="Paste text from your client to the remote host"
|
description={m.paste_modal_paste_text_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -143,7 +144,7 @@ export default function PasteModal() {
|
||||||
>
|
>
|
||||||
<TextAreaWithLabel
|
<TextAreaWithLabel
|
||||||
ref={TextAreaRef}
|
ref={TextAreaRef}
|
||||||
label="Paste from host"
|
label={m.paste_modal_paste_from_host()}
|
||||||
rows={4}
|
rows={4}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
maxLength={pasteMaxLength}
|
maxLength={pasteMaxLength}
|
||||||
|
|
@ -176,7 +177,7 @@ export default function PasteModal() {
|
||||||
<div className="mt-2 flex items-center gap-x-2">
|
<div className="mt-2 flex items-center gap-x-2">
|
||||||
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
||||||
<span className="text-xs text-red-500 dark:text-red-400">
|
<span className="text-xs text-red-500 dark:text-red-400">
|
||||||
The following characters won't be pasted:{" "}
|
{m.paste_modal_invalid_chars_intro()}{" "}
|
||||||
{invalidChars.join(", ")}
|
{invalidChars.join(", ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -186,8 +187,8 @@ export default function PasteModal() {
|
||||||
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
|
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
type="number"
|
type="number"
|
||||||
label="Delay between keys"
|
label={m.paste_modal_delay_between_keys()}
|
||||||
placeholder="Delay between keys"
|
placeholder={m.paste_modal_delay_between_keys()}
|
||||||
min={50}
|
min={50}
|
||||||
max={65534}
|
max={65534}
|
||||||
value={delayValue}
|
value={delayValue}
|
||||||
|
|
@ -199,15 +200,14 @@ export default function PasteModal() {
|
||||||
<div className="mt-2 flex items-center gap-x-2">
|
<div className="mt-2 flex items-center gap-x-2">
|
||||||
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
||||||
<span className="text-xs text-red-500 dark:text-red-400">
|
<span className="text-xs text-red-500 dark:text-red-400">
|
||||||
Delay must be between 50 and 65534
|
{m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
Sending text using keyboard layout: {selectedKeyboard.isoCode}-
|
{m.paste_modal_sending_using_layout({ iso: selectedKeyboard.isoCode, name: selectedKeyboard.name })}
|
||||||
{selectedKeyboard.name}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -224,7 +224,7 @@ export default function PasteModal() {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="blank"
|
theme="blank"
|
||||||
text="Cancel"
|
text={m.cancel()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCancelPasteMode();
|
onCancelPasteMode();
|
||||||
close();
|
close();
|
||||||
|
|
@ -233,7 +233,7 @@ export default function PasteModal() {
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Confirm Paste"
|
text={m.paste_modal_confirm_paste()}
|
||||||
disabled={isPasteInProgress}
|
disabled={isPasteInProgress}
|
||||||
onClick={onConfirmPaste}
|
onClick={onConfirmPaste}
|
||||||
LeadingIcon={LuCornerDownLeft}
|
LeadingIcon={LuCornerDownLeft}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { LuPlus, LuArrowLeft } from "react-icons/lu";
|
import { LuPlus, LuArrowLeft } from "react-icons/lu";
|
||||||
|
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { m } from "@localizations/messages.js";
|
||||||
import { Button } from "@/components/Button";
|
import { InputFieldWithLabel } from "@components/InputField";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
interface AddDeviceFormProps {
|
interface AddDeviceFormProps {
|
||||||
onAddDevice: (name: string, macAddress: string) => void;
|
onAddDevice: (name: string, macAddress: string) => void;
|
||||||
|
|
@ -34,8 +35,8 @@ export default function AddDeviceForm({
|
||||||
>
|
>
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
ref={nameInputRef}
|
ref={nameInputRef}
|
||||||
placeholder="Plex Media Server"
|
placeholder={m.wake_on_lan_add_device_example_device_name()}
|
||||||
label="Device Name"
|
label={m.wake_on_lan_add_device_device_name()}
|
||||||
required
|
required
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setIsDeviceNameValid(e.target.validity.valid);
|
setIsDeviceNameValid(e.target.validity.valid);
|
||||||
|
|
@ -46,7 +47,7 @@ export default function AddDeviceForm({
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
ref={macInputRef}
|
ref={macInputRef}
|
||||||
placeholder="00:b0:d0:63:c2:26"
|
placeholder="00:b0:d0:63:c2:26"
|
||||||
label="MAC Address"
|
label={m.wake_on_lan_add_device_mac_address()}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
required
|
required
|
||||||
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
|
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
|
||||||
|
|
@ -82,14 +83,14 @@ export default function AddDeviceForm({
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Back"
|
text={m.wake_on_lan_add_device_back()}
|
||||||
LeadingIcon={LuArrowLeft}
|
LeadingIcon={LuArrowLeft}
|
||||||
onClick={() => setShowAddForm(false)}
|
onClick={() => setShowAddForm(false)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Save Device"
|
text={m.wake_on_lan_add_device_save_device()}
|
||||||
disabled={!isDeviceNameValid || !isMacAddressValid}
|
disabled={!isDeviceNameValid || !isMacAddressValid}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const deviceName = nameInputRef.current?.value || "";
|
const deviceName = nameInputRef.current?.value || "";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
|
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { m } from "@localizations/messages.js";
|
||||||
import Card from "@/components/Card";
|
import { Button } from "@components/Button";
|
||||||
import { FieldError } from "@/components/InputField";
|
import Card from "@components/Card";
|
||||||
|
import { FieldError } from "@components/InputField";
|
||||||
|
|
||||||
export interface StoredDevice {
|
export interface StoredDevice {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -46,7 +47,7 @@ export default function DeviceList({
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Wake"
|
text={m.wake_on_lan_device_list_wake()}
|
||||||
LeadingIcon={LuSend}
|
LeadingIcon={LuSend}
|
||||||
onClick={() => onSendMagicPacket(device.macAddress)}
|
onClick={() => onSendMagicPacket(device.macAddress)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -55,7 +56,7 @@ export default function DeviceList({
|
||||||
theme="danger"
|
theme="danger"
|
||||||
LeadingIcon={LuTrash2}
|
LeadingIcon={LuTrash2}
|
||||||
onClick={() => onDeleteDevice(index)}
|
onClick={() => onDeleteDevice(index)}
|
||||||
aria-label="Delete device"
|
aria-label={m.wake_on_lan_device_list_delete_device()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,11 +70,11 @@ export default function DeviceList({
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
|
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Add New Device"
|
text={m.wake_on_lan_device_list_add_new_device()}
|
||||||
onClick={() => setShowAddForm(true)}
|
onClick={() => setShowAddForm(true)}
|
||||||
LeadingIcon={LuPlus}
|
LeadingIcon={LuPlus}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { PlusCircleIcon } from "@heroicons/react/16/solid";
|
import { PlusCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { LuPlus } from "react-icons/lu";
|
import { LuPlus } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@/components/Card";
|
import { m } from "@localizations/messages.js";
|
||||||
import { Button } from "@/components/Button";
|
import Card from "@components/Card";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
export default function EmptyStateCard({
|
export default function EmptyStateCard({
|
||||||
onCancelWakeOnLanModal,
|
onCancelWakeOnLanModal,
|
||||||
|
|
@ -25,10 +26,10 @@ export default function EmptyStateCard({
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||||
No devices added
|
{m.wake_on_lan_empty_no_devices_added()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||||
Add a device to start using Wake-on-LAN
|
{m.wake_on_lan_empty_add_device_to_start()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -41,11 +42,11 @@ export default function EmptyStateCard({
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
|
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Add New Device"
|
text={m.wake_on_lan_empty_add_new_device()}
|
||||||
onClick={() => setShowAddForm(true)}
|
onClick={() => setShowAddForm(true)}
|
||||||
LeadingIcon={LuPlus}
|
LeadingIcon={LuPlus}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
|
|
||||||
|
import { m } from "@localizations/messages.js";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
import { useRTCStore, useUiStore } from "@hooks/stores";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
import EmptyStateCard from "./EmptyStateCard";
|
import EmptyStateCard from "./EmptyStateCard";
|
||||||
|
|
@ -35,12 +36,12 @@ export default function WakeOnLanModal() {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
const isInvalid = resp.error.data?.includes("invalid MAC address");
|
const isInvalid = resp.error.data?.includes("invalid MAC address");
|
||||||
if (isInvalid) {
|
if (isInvalid) {
|
||||||
setErrorMessage("Invalid MAC address");
|
setErrorMessage(m.wake_on_lan_invalid_mac());
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage("Failed to send Magic Packet");
|
setErrorMessage(m.wake_on_lan_failed_send_magic());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notifications.success("Magic Packet sent successfully");
|
notifications.success(m.wake_on_lan_magic_sent_success());
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +88,7 @@ export default function WakeOnLanModal() {
|
||||||
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
|
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error("Failed to add Wake-on-LAN device:", resp.error);
|
console.error("Failed to add Wake-on-LAN device:", resp.error);
|
||||||
setAddDeviceErrorMessage("Failed to add device");
|
setAddDeviceErrorMessage(m.wake_on_lan_failed_add_device());
|
||||||
} else {
|
} else {
|
||||||
setShowAddForm(false);
|
setShowAddForm(false);
|
||||||
syncStoredDevices();
|
syncStoredDevices();
|
||||||
|
|
@ -103,8 +104,8 @@ export default function WakeOnLanModal() {
|
||||||
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Wake On LAN"
|
title={m.wake_on_lan()}
|
||||||
description="Send a Magic Packet to wake up a remote device."
|
description={m.wake_on_lan_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showAddForm ? (
|
{showAddForm ? (
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
|
|
||||||
import SidebarHeader from "@/components/SidebarHeader";
|
import { m } from "@localizations/messages.js";
|
||||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
import { createChartArray, Metric } from "@components/Metric";
|
||||||
|
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||||
|
import SidebarHeader from "@components/SidebarHeader";
|
||||||
|
import { useRTCStore, useUiStore } from "@hooks/stores";
|
||||||
import { someIterable } from "@/utils";
|
import { someIterable } from "@/utils";
|
||||||
|
|
||||||
import { createChartArray, Metric } from "../Metric";
|
|
||||||
import { SettingsSectionHeader } from "../SettingsSectionHeader";
|
|
||||||
|
|
||||||
export default function ConnectionStatsSidebar() {
|
export default function ConnectionStatsSidebar() {
|
||||||
const { sidebarView, setSidebarView } = useUiStore();
|
const { sidebarView, setSidebarView } = useUiStore();
|
||||||
const {
|
const {
|
||||||
|
|
@ -95,7 +95,7 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
|
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
|
||||||
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
|
<SidebarHeader title={m.connection_stats_sidebar()} 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="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">
|
<div className="space-y-4">
|
||||||
{sidebarView === "connection-stats" && (
|
{sidebarView === "connection-stats" && (
|
||||||
|
|
@ -103,12 +103,12 @@ export default function ConnectionStatsSidebar() {
|
||||||
{/* Connection Group */}
|
{/* Connection Group */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingsSectionHeader
|
<SettingsSectionHeader
|
||||||
title="Connection"
|
title={m.connection_stats_connection()}
|
||||||
description="The connection between the client and the JetKVM."
|
description={m.connection_stats_connection_description()}
|
||||||
/>
|
/>
|
||||||
<Metric
|
<Metric
|
||||||
title="Round-Trip Time"
|
title={m.connection_stats_round_trip_time()}
|
||||||
description="Round-trip time for the active ICE candidate pair between peers."
|
description={m.connection_stats_round_trip_time_description()}
|
||||||
stream={iceCandidatePairStats}
|
stream={iceCandidatePairStats}
|
||||||
metric="currentRoundTripTime"
|
metric="currentRoundTripTime"
|
||||||
map={x => ({
|
map={x => ({
|
||||||
|
|
@ -123,16 +123,16 @@ export default function ConnectionStatsSidebar() {
|
||||||
{/* Video Group */}
|
{/* Video Group */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SettingsSectionHeader
|
<SettingsSectionHeader
|
||||||
title="Video"
|
title={m.connection_stats_video()}
|
||||||
description="The video stream from the JetKVM to the client."
|
description={m.connection_stats_video_description()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* RTP Jitter */}
|
{/* RTP Jitter */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Network Stability"
|
title={m.connection_stats_network_stability()}
|
||||||
badge="Jitter"
|
badge={m.connection_stats_badge_jitter()}
|
||||||
badgeTheme="light"
|
badgeTheme="light"
|
||||||
description="How steady the flow of inbound video packets is across the network."
|
description={m.connection_stats_network_stability_description()}
|
||||||
stream={inboundVideoRtpStats}
|
stream={inboundVideoRtpStats}
|
||||||
metric="jitter"
|
metric="jitter"
|
||||||
map={x => ({
|
map={x => ({
|
||||||
|
|
@ -145,9 +145,9 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
{/* Playback Delay */}
|
{/* Playback Delay */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Playback Delay"
|
title={m.connection_stats_playback_delay()}
|
||||||
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly."
|
description={m.connection_stats_playback_delay_description()}
|
||||||
badge="Jitter Buffer Avg. Delay"
|
badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
|
||||||
badgeTheme="light"
|
badgeTheme="light"
|
||||||
data={jitterBufferAvgDelayData}
|
data={jitterBufferAvgDelayData}
|
||||||
gate={inboundVideoRtpStats}
|
gate={inboundVideoRtpStats}
|
||||||
|
|
@ -167,8 +167,8 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
{/* Packets Lost */}
|
{/* Packets Lost */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Packets Lost"
|
title={m.connection_stats_packets_lost()}
|
||||||
description="Count of lost inbound video RTP packets."
|
description={m.connection_stats_packets_lost_description()}
|
||||||
stream={inboundVideoRtpStats}
|
stream={inboundVideoRtpStats}
|
||||||
metric="packetsLost"
|
metric="packetsLost"
|
||||||
domain={[0, 100]}
|
domain={[0, 100]}
|
||||||
|
|
@ -177,8 +177,8 @@ export default function ConnectionStatsSidebar() {
|
||||||
|
|
||||||
{/* Frames Per Second */}
|
{/* Frames Per Second */}
|
||||||
<Metric
|
<Metric
|
||||||
title="Frames per second"
|
title={m.connection_stats_frames_per_second()}
|
||||||
description="Number of inbound video frames displayed per second."
|
description={m.connection_stats_frames_per_second_description()}
|
||||||
stream={inboundVideoRtpStats}
|
stream={inboundVideoRtpStats}
|
||||||
metric="framesPerSecond"
|
metric="framesPerSecond"
|
||||||
domain={[0, 80]}
|
domain={[0, 80]}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
|
|
||||||
|
|
||||||
interface NotificationOptions {
|
interface NotificationOptions {
|
||||||
duration?: number;
|
duration?: number;
|
||||||
// Add other options as needed
|
// Add other options as needed
|
||||||
|
|
@ -34,7 +33,7 @@ const ToastContent = ({
|
||||||
const notifications = {
|
const notifications = {
|
||||||
success: (message: string, options?: NotificationOptions) => {
|
success: (message: string, options?: NotificationOptions) => {
|
||||||
return toast.custom(
|
return toast.custom(
|
||||||
t => (
|
(t: Toast) => (
|
||||||
<ToastContent
|
<ToastContent
|
||||||
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
|
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
|
||||||
message={message}
|
message={message}
|
||||||
|
|
@ -47,7 +46,7 @@ const notifications = {
|
||||||
|
|
||||||
error: (message: string, options?: NotificationOptions) => {
|
error: (message: string, options?: NotificationOptions) => {
|
||||||
return toast.custom(
|
return toast.custom(
|
||||||
t => (
|
(t: Toast) => (
|
||||||
<ToastContent
|
<ToastContent
|
||||||
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
|
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
|
||||||
message={message}
|
message={message}
|
||||||
|
|
@ -64,9 +63,9 @@ function useMaxToasts(max: number) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
toasts
|
toasts
|
||||||
.filter(t => t.visible) // Only consider visible toasts
|
.filter((t: Toast) => t.visible) // Only consider visible toasts
|
||||||
.filter((_, i) => i >= max) // Is toast index over limit?
|
.filter((_: Toast, i: number) => i >= max) // Is toast index over limit?
|
||||||
.forEach(t => toast.dismiss(t.id)); // Dismiss – Use toast.remove(t.id) for no exit animation
|
.forEach((t: Toast) => toast.dismiss(t.id)); // Dismiss – Use toast.remove(t.id) for no exit animation
|
||||||
}, [toasts, max]);
|
}, [toasts, max]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue