Compare commits

...

4 Commits

Author SHA1 Message Date
Marc Brooks a3c1937776
Merge ea43caae27 into b144d9926f 2025-10-08 21:32:26 +00:00
Marc Brooks ea43caae27
Localized all components, hooks, providers, hooks 2025-10-08 16:29:56 -05:00
Marc Brooks 2037c9d478
File formatting pass 2025-10-08 16:28:16 -05:00
Marc Brooks b2d657beaa Update Chinese translations
Accidentally lost the changes that @ym provided, brought them back
2025-10-08 18:22:25 +00:00
65 changed files with 766 additions and 588 deletions

View File

@ -1,144 +1,323 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "Oh no!",
"something_went_wrong": "Something went wrong. Please try again later or contact support",
"jetkvm_logo": "JetKVM Logo",
"load": "Load",
"unknown_error": "Unknown error",
"close": "Close",
"attach": "Attach",
"cancel": "Cancel",
"action_bar_virtual_media": "Virtual Media",
"action_bar_paste_text": "Paste text",
"action_bar_web_terminal": "Web Terminal",
"action_bar_wake_on_lan": "Wake on LAN",
"action_bar_virtual_keyboard": "Virtual Keyboard",
"action_bar_extension": "Extension",
"close": "Close",
"confirm": "Confirm",
"connect_to_kvm": "Connect to KVM",
"default": "Default",
"delete": "Delete",
"deregister_from_cloud": "Deregister from cloud",
"detach": "Detach",
"dhcp_server": "DHCP Server",
"dns_servers": "DNS Servers",
"hide": "Hide",
"info_caps_lock": "Caps Lock",
"info_compose": "Compose",
"info_hdmi_state": "HDMI State:",
"info_hidrpc_state": "HidRPC State:",
"info_kana": "Kana",
"info_keys": "Keys:",
"info_last_move": "Last Move:",
"info_num_lock": "Num Lock",
"info_paste_enabled": "Enabled",
"info_paste_mode": "Paste Mode:",
"info_pointer": "Pointer:",
"info_relayed_by_cloudflare": "Relayed by Cloudflare",
"info_resolution": "Resolution:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Shift",
"info_usb_state": "USB State:",
"info_video_size": "Video Size:",
"input_disabled": "Input disabled",
"ip_address": "IP Address",
"last_online": "Last online {time}",
"load": "Load",
"log_out": "Log out",
"logged_in_as": "Logged in as",
"never_seen_online": "Never seen online",
"no_results_found": "No results found",
"not_available": "N/A",
"not_found": "Not found",
"ntp_servers": "NTP Servers",
"oh_no": "Oh no!",
"online": "Online",
"page_not_found_description": "The page you were looking for does not exist.",
"rename": "Rename",
"saving": "Saving...",
"search_placeholder": "Search...",
"something_went_wrong": "Something went wrong. Please try again later or contact support",
"subnet_mask": "Subnet Mask",
"troubleshoot_connection": "Troubleshoot Connection",
"unknown_error": "Unknown error",
"update_in_progress": "Update in Progress",
"updating_leave_device_on": "Please don't turn off your device...",
"usb": "USB",
"view_details": "View Details",
"action_bar_connection_stats": "Connection Stats",
"action_bar_settings": "Settings",
"action_bar_fullscreen": "Fullscreen",
"action_bar_exit_fullscreen": "Exit Fullscreen",
"extensions_popover_extensions": "Extensions",
"action_bar_extension": "Extension",
"action_bar_fullscreen": "Fullscreen",
"action_bar_paste_text": "Paste text",
"action_bar_settings": "Settings",
"action_bar_virtual_keyboard": "Virtual Keyboard",
"action_bar_virtual_media": "Virtual Media",
"action_bar_wake_on_lan": "Wake on LAN",
"action_bar_web_terminal": "Web Terminal",
"extension_popover_load_and_manage_extensions": "Load and manage your extensions",
"extension_popover_set_error_notification": "Failed to set active extension: {error}",
"extension_popover_unload_extension": "Unload Extension",
"extension_popover_load_and_manage_extensions": "Load and manage your extensions",
"extensions_atx_power_control": "ATX Power Control",
"extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
"extensions_dc_power_control": "DC Power Control",
"extensions_dc_power_control_description": "Control your DC Power extension",
"extension_serial_console": "Serial Console",
"extension_serial_console_description": "Access your serial console extension",
"extension_serial_console": "Serial Console",
"extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
"extensions_atx_power_control": "ATX Power Control",
"extensions_dc_power_control_description": "Control your DC Power extension",
"extensions_dc_power_control": "DC Power Control",
"extensions_popover_extensions": "Extensions",
"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",
"atx_power_control_short_power_button": "Short Press",
"atx_power_control_long_power_button": "Long Press",
"atx_power_control_reset_button": "Reset",
"atx_power_control_power_led": "Power LED",
"atx_power_control_hdd_led": "HDD LED",
"atx_power_control_long_power_button": "Long Press",
"atx_power_control_power_button": "Power",
"atx_power_control_power_led": "Power LED",
"atx_power_control_reset_button": "Reset",
"atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
"atx_power_control_short_power_button": "Short Press",
"dc_power_control_current_unit": "A",
"dc_power_control_current": "Current",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
"dc_power_control_power_off_button": "Power Off",
"dc_power_control_power_off_state": "Power OFF",
"dc_power_control_power_on_button": "Power On",
"dc_power_control_power_on_state": "Power ON",
"dc_power_control_power_unit": "W",
"dc_power_control_power": "Power",
"dc_power_control_restore_last_state": "Last State",
"dc_power_control_restore_power_state": "Restore Power Loss",
"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_power_on_button": "Power On",
"dc_power_control_power_off_button": "Power Off",
"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",
"serial_console_open_console": "Open Console",
"dc_power_control_voltage": "Voltage",
"serial_console_baud_rate": "Baud Rate",
"serial_console_configure_description": "Configure your serial console settings",
"serial_console_data_bits": "Data Bits",
"serial_console_stop_bits": "Stop Bits",
"serial_console_parity": "Parity",
"serial_console_get_settings_error": "Failed to get serial console settings: {error}",
"serial_console_open_console": "Open Console",
"serial_console_parity_even": "Even Parity",
"serial_console_parity_odd": "Odd Parity",
"serial_console_parity_none": "No Parity",
"serial_console_parity_mark": "Mark Parity",
"serial_console_parity_none": "No Parity",
"serial_console_parity_odd": "Odd Parity",
"serial_console_parity_space": "Space Parity",
"serial_console_parity": "Parity",
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
"serial_console_stop_bits": "Stop Bits",
"wake_on_lan_add_device_back": "Back",
"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_confirm_paste": "Confirm Paste",
"paste_modal_delay_between_keys": "Delay between keys",
"paste_modal_delay_out_of_range": "Delay must be between {min} and {max}",
"paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
"paste_modal_paste_from_host": "Paste from host",
"paste_modal_paste_text_description": "Paste text from your client to the remote host",
"paste_modal_paste_text": "Paste text",
"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",
"paste_modal_failed_paste": "Failed to paste text: {error}",
"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_mounted_from_storage": "Mounted from JetKVM Storage",
"mount_no_mounted_media": "No mounted media",
"mount_streaming_from_url": "Streaming from URL",
"mount_unmount": "Unmount",
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
"mount_virtual_media": "Virtual 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",
"mount_mode_disk": "Disk",
"mount_mounted_as": "Mounted as",
"mount_unmount_error": "Failed to unmount image: {error}",
"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_device_list_add_new_device": "Add New Device",
"wake_on_lan_device_list_delete_device": "Delete device",
"wake_on_lan_device_list_wake": "Wake",
"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.",
"wake_on_lan_empty_no_devices_added": "No devices added",
"wake_on_lan_failed_add_device": "Failed to add device",
"wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
"wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"wake_on_lan": "Wake On LAN",
"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_badge_jitter": "Jitter",
"connection_stats_connection_description": "The connection between the client and the JetKVM.",
"connection_stats_connection": "Connection",
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.",
"connection_stats_frames_per_second": "Frames per second",
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second."
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
"connection_stats_network_stability": "Network Stability",
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
"connection_stats_packets_lost": "Packets Lost",
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
"connection_stats_playback_delay": "Playback Delay",
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
"connection_stats_round_trip_time": "Round-Trip Time",
"connection_stats_sidebar": "Connection Stats",
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
"connection_stats_video": "Video",
"peer_connection_connected": "Connected",
"peer_connection_connecting": "Connecting",
"peer_connection_disconnected": "Disconnected",
"peer_connection_error": "Connection error",
"peer_connection_closing": "Closing",
"peer_connection_failed": "Connection failed",
"peer_connection_closed": "Closed",
"peer_connection_new": "Connecting",
"usb_device_keyboard_mouse_and_mass_storage": "Keyboard, Mouse and Mass Storage",
"usb_device_keyboard_only": "Keyboard Only",
"usb_device_custom": "Custom",
"usb_device_title": "USB Device",
"usb_device_description": "USB devices to emulate on the target computer",
"usb_device_classes_title": "Classes",
"usb_device_classes_description": "USB device classes in the composite device",
"usb_device_enable_keyboard_title": "Enable Keyboard",
"usb_device_enable_keyboard_description": "Enable Keyboard",
"usb_device_enable_absolute_mouse_title": "Enable Absolute Mouse (Pointer)",
"usb_device_enable_absolute_mouse_description": "Enable Absolute Mouse (Pointer)",
"usb_device_enable_relative_mouse_title": "Enable Relative Mouse",
"usb_device_enable_relative_mouse_description": "Enable Relative Mouse",
"usb_device_enable_mass_storage_title": "Enable USB Mass Storage",
"usb_device_enable_mass_storage_description": "Sometimes it might need to be disabled to prevent issues with certain devices",
"usb_device_update_classes": "Update USB Classes",
"usb_device_restore_default": "Restore to Default",
"usb_device_failed_load": "Failed to load USB devices: {error}",
"usb_device_failed_set": "Failed to set USB devices: {error}",
"usb_device_updated": "USB Devices updated",
"updates_failed_check": "Failed to check for updates: {error}",
"updates_failed_get_device_version": "Failed to get device version: {error}",
"auth_connect_to_cloud_action": "Log in & Connect device",
"auth_connect_to_cloud_description": "Unlock remote access and advanced features for your device",
"auth_connect_to_cloud": "Connect your JetKVM to the cloud",
"auth_signup_connect_to_cloud_action": "Signup & Connect device",
"auth_login_action": "Log in",
"auth_login_description": "Log in to access and manage your devices securely",
"auth_login": "Log in to your JetKVM account",
"auth_header_cta_already_have_account": "Already have an account?",
"auth_header_cta_dont_have_account": "Don't have an account?",
"auth_header_cta_new_to_jetkvm": "New to JetKVM?",
"auth_signup_create_account_action": "Create Account",
"auth_signup_create_account_description": "Create your account and start managing your devices with ease.",
"auth_signup_create_account": "Create your JetKVM account",
"video_overlay_autoplay_permissions_required": "Autoplay permissions required",
"video_overlay_conn_check_cables": "Check all cable connections for any loose or damaged wires",
"video_overlay_conn_ensure_network": "Ensure your network connection is stable and active",
"video_overlay_conn_restart": "Try restarting both the device and your computer",
"video_overlay_conn_verify_power": "Verify that the device is powered on and properly connected",
"video_overlay_connection_issue_title": "Connection Issue Detected",
"video_overlay_enable_autoplay_settings": "Please adjust browser settings to enable autoplay",
"video_overlay_hdmi_error_title": "HDMI signal error detected.",
"video_overlay_hdmi_incompatible_resolution": "Incompatible resolution or refresh rate settings",
"video_overlay_hdmi_loose_faulty": "A loose or faulty HDMI connection",
"video_overlay_hdmi_source_issue": "Issues with the source device's HDMI output",
"video_overlay_learn_more": "Learn more",
"video_overlay_loading_stream": "Loading video stream...",
"video_overlay_manually_start_stream": "Manually start stream",
"video_overlay_no_hdmi_adapter_compat": "If using an adapter, ensure it's compatible and functioning correctly",
"video_overlay_no_hdmi_ensure_cable": "Ensure the HDMI cable securely connected at both ends",
"video_overlay_no_hdmi_ensure_power": "Ensure source device is powered on and outputting a signal",
"video_overlay_no_hdmi_signal": "No HDMI signal detected.",
"video_overlay_pointerlock_click_to_enable": "Click on the video to enable mouse control",
"video_overlay_retrying_connection": "Retrying connection...",
"video_overlay_troubleshooting_guide": "Troubleshooting Guide",
"video_overlay_try_again": "Try again",
"video_pointer_lock_enabled": "Pointer lock enabled — press Escape to unlock",
"video_pointer_lock_disabled": "Pointer lock disabled",
"ipv6_information": "IPv6 Information",
"ipv6_link_local": "Link-local",
"ipv6_addresses": "IPv6 Addresses",
"ipv6_address_label": "Address",
"ipv6_valid_lifetime": "Valid Lifetime",
"ipv6_preferred_lifetime": "Preferred Lifetime",
"dhcp_lease_boot_file": "Boot File",
"dhcp_lease_boot_next_server": "Boot Next Server",
"dhcp_lease_boot_server_name": "Boot Server Name",
"dhcp_lease_broadcast": "Broadcast",
"dhcp_lease_domain": "Domain",
"dhcp_lease_gateway": "Gateway",
"dhcp_lease_header": "DHCP Lease Information",
"dhcp_lease_hostname": "Hostname",
"dhcp_lease_lease_expires": "Lease Expires",
"dhcp_lease_maximum_transfer_unit": "MTU",
"dhcp_lease_renew": "Renew DHCP Lease",
"dhcp_lease_time_to_live": "TTL",
"step_counter_step": "Step {step}",
"macro_add_step": "Add Step{maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "At least one step must have keys or modifiers",
"macro_at_least_one_step_required": "At least one step is required",
"macro_max_steps_error": "You can only add a maximum of {max} steps per macro.",
"macro_max_steps_reached": "({max} max)",
"macro_name_label": "Macro Name",
"macro_name_required": "Name is required",
"macro_name_too_long": "Name must be less than 50 characters",
"macro_please_fix_validation_errors": "Please fix the validation errors",
"macro_save": "Save Macro",
"macro_save_error": "An error occurred while saving.",
"macro_step_count": "{steps} / {max} steps",
"macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
"macro_steps_label": "Steps",
"macro_step_duration_description": "Time to wait before executing the next step.",
"macro_step_duration_label": "Step Duration",
"macro_step_keys_description": "Maximum {max} keys per step.",
"macro_step_keys_label": "Keys",
"macro_step_max_keys_reached": "Maximum keys reached",
"macro_step_modifiers_description": "What modifiers (Shift/Ctrl/Alt/Meta) are pressed during this step.",
"macro_step_modifiers_label": "Modifiers",
"macro_step_no_matching_keys_found": "No matching keys found",
"macro_step_search_for_key": "Search for key...",
"jiggler_examples_label": "Examples",
"jiggler_more_examples": "More examples",
"jiggler_cron_schedule_label": "Cron Schedule",
"jiggler_cron_schedule_description": "Cron expression for scheduling",
"jiggler_example_business_hours_late": "Business Hours 9-17",
"jiggler_example_business_hours_early": "Business Hours 8-17",
"jiggler_inactivity_limit_label": "Inactivity Limit Seconds",
"jiggler_inactivity_limit_description": "Inactivity time before jiggle",
"jiggler_random_delay_label": "Random delay",
"jiggler_random_delay_description": "To avoid recognizable patterns",
"jiggler_timezone_label": "Timezone",
"jiggler_timezone_description": "Timezone for cron schedule",
"jiggler_save_jiggler_config": "Save Jiggler Config",
"metric_waiting_for_data": "Waiting for data...",
"metric_not_supported": "Metric not supported",
"usb_config_custom": "Custom",
"usb_config_default": "JetKVM Default",
"usb_config_dell": "Dell Multimedia Pro Keyboard",
"usb_config_failed_load": "Failed to load USB Config: {error}",
"usb_config_failed_set": "Failed to set usb config: {error}",
"usb_config_identifiers_description": "USB device identifiers exposed to the target computer",
"usb_config_identifiers_title": "Identifiers",
"usb_config_logitech": "Logitech Universal Adapter",
"usb_config_manufacturer_label": "Manufacturer",
"usb_config_manufacturer_placeholder": "Enter Manufacturer",
"usb_config_microsoft": "Microsoft Wireless MultiMedia Keyboard",
"usb_config_product_id_label": "Product ID",
"usb_config_product_id_placeholder": "Enter Product ID",
"usb_config_product_name_label": "Product Name",
"usb_config_product_name_placeholder": "Enter Product Name",
"usb_config_restore_default": "Restore to Default",
"usb_config_serial_number_label": "Serial Number",
"usb_config_serial_number_placeholder": "Enter Serial Number",
"usb_config_set_success": "USB Config set to {manufacturer} {product}",
"usb_config_update_identifiers": "Update USB Identifiers",
"usb_config_vendor_id_label": "Vendor ID",
"usb_config_vendor_id_placeholder": "Enter Vendor ID",
"usb_state_connected": "Connected",
"usb_state_connecting": "Connecting",
"usb_state_disconnected": "Disconnected",
"usb_state_low_power_mode": "Low power mode",
"virtual_keyboard_header": "Virtual Keyboard",
"virtual_keyboard_description": "Use the virtual keyboard to send special keys or key combinations to the remote computer."
}

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

@ -4,6 +4,7 @@ import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-ic
import { FaKeyboard } from "react-icons/fa6";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { CommandLineIcon } from "@heroicons/react/20/solid";
import { cx } from "@/cva.config";
import {
useHidStore,
@ -11,14 +12,13 @@ import {
useSettingsStore,
useUiStore,
} from "@hooks/stores";
import { cx } from "@/cva.config";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { Button } from "@components/Button";
import Container from "@components/Container";
import PasteModal from "@components/popovers/PasteModal";
import WakeOnLanModal from "@components/popovers/WakeOnLan/Index";
import MountPopopover from "@components/popovers/MountPopover";
import ExtensionPopover from "@components/popovers/ExtensionPopover";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { m } from "@localizations/messages.js";
export default function Actionbar({

View File

@ -1,11 +1,10 @@
import React, { JSX } from "react";
import { Link, useNavigation } from "react-router";
import type { FetcherWithComponents, LinkProps } from "react-router";
import ExtLink from "@/components/ExtLink";
import LoadingSpinner from "@/components/LoadingSpinner";
import { Link, type FetcherWithComponents, type LinkProps, useNavigation } from "react-router";
import { cva, cx } from "@/cva.config";
import ExtLink from "@components/ExtLink";
import LoadingSpinner from "@components/LoadingSpinner";
const sizes = {
XS: "h-[28px] px-2 text-xs",
SM: "h-[36px] px-3 text-[13px]",

View File

@ -2,7 +2,7 @@ import type { Ref } from "react";
import React, { forwardRef, JSX } from "react";
import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel";
import FieldLabel from "@components/FieldLabel";
import { cva, cx } from "@/cva.config";
const sizes = {

View File

@ -7,10 +7,10 @@ import {
ComboboxOptions,
} from "@headlessui/react";
import { m } from "@localizations/messages.js";
import Card from "@components/Card";
import { cva } from "@/cva.config";
import Card from "./Card";
export interface ComboboxOption {
value: string;
label: string;
@ -44,11 +44,11 @@ export function Combobox({
displayValue,
options,
disabled = false,
placeholder = "Search...",
emptyMessage = "No results found",
placeholder = m.search_placeholder(),
emptyMessage = m.no_results_found(),
size = "MD",
onChange,
disabledMessage = "Input disabled",
disabledMessage = m.input_disabled(),
...otherProps
}: ComboboxProps) {
const inputRef = useRef<HTMLInputElement>(null);

View File

@ -4,8 +4,9 @@ import {
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { Button } from "@/components/Button";
import Modal from "@/components/Modal";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button";
import Modal from "@components/Modal";
import { cx } from "@/cva.config";
type Variant = "danger" | "success" | "warning" | "info";
@ -63,8 +64,8 @@ export function ConfirmDialog({
title,
description,
variant = "info",
confirmText = "Confirm",
cancelText = "Cancel",
confirmText = m.confirm(),
cancelText = m.cancel(),
onConfirm,
isConfirming = false,
}: ConfirmDialogProps) {

View File

@ -1,9 +1,10 @@
import { LuRefreshCcw } from "react-icons/lu";
import { Button } from "@/components/Button";
import { GridCard } from "@/components/Card";
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
import { NetworkState } from "@/hooks/stores";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
import { NetworkState } from "@hooks/stores";
import { m } from "@localizations/messages.js";
export default function DhcpLeaseCard({
networkState,
@ -17,7 +18,7 @@ export default function DhcpLeaseCard({
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
<div className="space-y-3">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
DHCP Lease Information
{m.dhcp_lease_header()}
</h3>
<div className="flex gap-x-6 gap-y-2">
@ -25,7 +26,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.ip && (
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
IP Address
{m.ip_address()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.ip}
@ -36,7 +37,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.netmask && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Subnet Mask
{m.subnet_mask()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.netmask}
@ -47,7 +48,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.dns && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
DNS Servers
{m.dns_servers()}
</span>
<span className="text-right text-sm font-medium">
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
@ -58,7 +59,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.broadcast && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Broadcast
{m.dhcp_lease_broadcast()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.broadcast}
@ -69,7 +70,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.domain && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Domain
{m.dhcp_lease_domain()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.domain}
@ -81,7 +82,7 @@ export default function DhcpLeaseCard({
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
NTP Servers
{m.ntp_servers()}
</div>
<div className="shrink text-right text-sm font-medium">
{networkState?.dhcp_lease?.ntp_servers.map(server => (
@ -94,7 +95,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.hostname && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Hostname
{m.dhcp_lease_hostname()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.hostname}
@ -108,7 +109,7 @@ export default function DhcpLeaseCard({
networkState?.dhcp_lease?.routers.length > 0 && (
<div className="flex justify-between pt-2">
<span className="text-sm text-slate-600 dark:text-slate-400">
Gateway
{m.dhcp_lease_gateway()}
</span>
<span className="text-right text-sm font-medium">
{networkState?.dhcp_lease?.routers.map(router => (
@ -121,7 +122,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.server_id && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
DHCP Server
{m.dhcp_server()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.server_id}
@ -132,7 +133,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.lease_expiry && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Lease Expires
{m.dhcp_lease_lease_expires()}
</span>
<span className="text-sm font-medium">
<LifeTimeLabel
@ -146,7 +147,7 @@ export default function DhcpLeaseCard({
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.mtu}
{m.dhcp_lease_maximum_transfer_unit()}
</span>
</div>
)}
@ -155,7 +156,7 @@ export default function DhcpLeaseCard({
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">TTL</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.ttl}
{m.dhcp_lease_time_to_live()}
</span>
</div>
)}
@ -163,7 +164,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.bootp_next_server && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Boot Next Server
{m.dhcp_lease_boot_next_server()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.bootp_next_server}
@ -174,7 +175,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.bootp_server_name && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Boot Server Name
{m.dhcp_lease_boot_server_name()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.bootp_server_name}
@ -185,7 +186,7 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.bootp_file && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Boot File
{m.dhcp_lease_boot_file()}
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.bootp_file}
@ -194,13 +195,12 @@ export default function DhcpLeaseCard({
)}
</div>
</div>
<div>
<Button
size="SM"
theme="light"
className="text-red-500"
text="Renew DHCP Lease"
text={m.dhcp_lease_renew()}
LeadingIcon={LuRefreshCcw}
onClick={() => setShowRenewLeaseConfirm(true)}
/>

View File

@ -1,8 +1,7 @@
import React from "react";
import { GridCard } from "@/components/Card";
import { cx } from "../cva.config";
import { GridCard } from "@components/Card";
import { cx } from "@/cva.config";
interface Props {
IconElm?: React.FC<{ className: string | undefined }>;

View File

@ -1,6 +1,6 @@
import { useEffect } from "react";
import { useFeatureFlag } from "../hooks/useFeatureFlag";
import { useFeatureFlag } from "@hooks/useFeatureFlag";
export function FeatureFlag({
minAppVersion,

View File

@ -1,7 +1,7 @@
import React from "react";
import clsx from "clsx";
import { useNavigation } from "react-router";
import type { FetcherWithComponents } from "react-router";
import clsx from "clsx";
export default function Fieldset({
children,

View File

@ -3,20 +3,19 @@ import { useNavigate } from "react-router";
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { LuMonitorSmartphone } from "react-icons/lu";
import LogoBlueIcon from "@assets/logo-blue.svg";
import LogoWhiteIcon from "@assets/logo-white.svg";
import Container from "@/components/Container";
import Card from "@/components/Card";
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
import LogoBlueIcon from "@/assets/logo-blue.svg";
import LogoWhiteIcon from "@/assets/logo-white.svg";
import USBStateStatus from "@components/USBStateStatus";
import { useHidStore, useRTCStore, useUserStore } from "@hooks/stores";
import Card from "@components/Card";
import Container from "@components/Container";
import { LinkButton } from "@components/Button";
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
import USBStateStatus from "@components/USBStateStatus";
import { CLOUD_API, DEVICE_API } from "@/ui.config";
import api from "../api";
import { isOnDevice } from "../main";
import { LinkButton } from "./Button";
import api from "@/api";
import { isOnDevice } from "@/main";
import { m } from "@localizations/messages.js";
interface NavbarProps {
isLoggedIn: boolean;
@ -131,7 +130,7 @@ export default function DashboardNavbar({
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
<div className="p-2">
<div className="font-display text-xs">
Logged in as
{m.logged_in_as()}
</div>
<div className="font-display max-w-[200px] truncate text-sm font-semibold">
{userEmail}
@ -146,7 +145,7 @@ export default function DashboardNavbar({
>
<button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
<ArrowLeftEndOnRectangleIcon className="size-4" />
<div className="font-display">Log out</div>
<div className="font-display">{m.log_out()}</div>
</button>
</div>
</Card>

View File

@ -1,6 +1,5 @@
import { useEffect, useMemo } from "react";
import { cx } from "@/cva.config";
import {
useHidStore,
useMouseStore,
@ -8,9 +7,11 @@ import {
useSettingsStore,
useVideoStore,
VideoState
} from "@/hooks/stores";
} from "@hooks/stores";
import { useHidRpc } from "@hooks/useHidRpc";
import { keys, modifiers } from "@/keyboardMappings";
import { useHidRpc } from "@/hooks/useHidRpc";
import { cx } from "@/cva.config";
import { m } from "@localizations/messages.js";
export default function InfoBar() {
const { keysDownState } = useHidStore();
@ -41,13 +42,16 @@ export default function InfoBar() {
const { hdmiState } = useVideoStore();
const displayKeys = useMemo(() => {
if (!showPressedKeys)
return "";
if (!showPressedKeys) return "";
const activeModifierMask = keysDownState.modifier || 0;
const keysDown = keysDownState.keys || [];
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
const modifierNames = Object.entries(modifiers)
.filter(([_, mask]) => (activeModifierMask & mask) !== 0)
.map(([name]) => name);
const keyNames = Object.entries(keys)
.filter(([_, value]) => keysDown.includes(value))
.map(([name]) => name);
return [...modifierNames, ...keyNames].join(", ");
}, [keysDownState, showPressedKeys]);
@ -59,76 +63,75 @@ export default function InfoBar() {
<div className="flex flex-wrap items-center pl-2 gap-x-4">
{debugMode ? (
<div className="flex">
<span className="text-xs font-semibold">Resolution:</span>{" "}
<span className="text-xs font-semibold">{m.info_resolution()}</span>{" "}
<span className="text-xs">{videoSize}</span>
</div>
) : null}
{debugMode ? (
<div className="flex">
<span className="text-xs font-semibold">Video Size: </span>
<span className="text-xs font-semibold">{m.info_video_size()}</span>
<span className="text-xs">{videoClientSize}</span>
</div>
) : null}
{(debugMode && mouseMode == "absolute") ? (
<div className="flex w-[118px] items-center gap-x-1">
<span className="text-xs font-semibold">Pointer:</span>
<span className="text-xs">
{mouseX},{mouseY}
</span>
<span className="text-xs font-semibold">{m.info_pointer()}</span>
<span className="text-xs">{mouseX},{mouseY}</span>
</div>
) : null}
{(debugMode && mouseMode == "relative") ? (
<div className="flex w-[118px] items-center gap-x-1">
<span className="text-xs font-semibold">Last Move:</span>
<span className="text-xs font-semibold">{m.info_last_move()}</span>
<span className="text-xs">
{mouseMove ?
`${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` :
"N/A"}
{mouseMove ? `${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` : "N/A"}
</span>
</div>
) : null}
{debugMode && (
<div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">USB State:</span>
<span className="text-xs font-semibold">{m.info_usb_state()}</span>
<span className="text-xs">{usbState}</span>
</div>
)}
{debugMode && (
<div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">HDMI State:</span>
<span className="text-xs font-semibold">{m.info_hdmi_state()}</span>
<span className="text-xs">{hdmiState}</span>
</div>
)}
{debugMode && (
<div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">HidRPC State:</span>
<span className="text-xs font-semibold">{m.info_hidrpc_state()}</span>
<span className="text-xs">{rpcHidStatus}</span>
</div>
)}
{isPasteInProgress && (
<div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">Paste Mode:</span>
<span className="text-xs">Enabled</span>
<span className="text-xs font-semibold">{m.info_paste_mode()}</span>
<span className="text-xs">{m.info_paste_enabled()}</span>
</div>
)}
{showPressedKeys && (
<div className="flex items-center gap-x-1">
<span className="text-xs font-semibold">Keys:</span>
<h2 className="text-xs">
{displayKeys}
</h2>
<span className="text-xs font-semibold">{m.info_keys()}</span>
<h2 className="text-xs">{displayKeys}</h2>
</div>
)}
</div>
</div>
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
{isTurnServerInUse && (
<div className="shrink-0 p-1 px-1.5 text-xs text-black dark:text-white">
Relayed by Cloudflare
{m.info_relayed_by_cloudflare()}
</div>
)}
@ -140,8 +143,9 @@ export default function InfoBar() {
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Caps Lock
{m.info_caps_lock()}
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
@ -150,8 +154,9 @@ export default function InfoBar() {
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Num Lock
{m.info_num_lock()}
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
@ -160,22 +165,19 @@ export default function InfoBar() {
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Scroll Lock
{m.info_scroll_lock()}
</div>
{keyboardLedState.compose ? (
<div className="shrink-0 p-1 px-1.5 text-xs">
Compose
</div>
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_compose()}</div>
) : null}
{keyboardLedState.kana ? (
<div className="shrink-0 p-1 px-1.5 text-xs">
Kana
</div>
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_kana()}</div>
) : null}
{keyboardLedState.shift ? (
<div className="shrink-0 p-1 px-1.5 text-xs">
Shift
</div>
<div className="shrink-0 p-1 px-1.5 text-xs">{m.info_shift()}</div>
) : null}
</div>
</div>

View File

@ -1,7 +1,7 @@
import { NetworkState } from "../hooks/stores";
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
import { GridCard } from "./Card";
import { NetworkState } from "@hooks/stores";
import { GridCard } from "@components/Card";
import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
import { m } from "@localizations/messages.js";
export default function Ipv6NetworkCard({
networkState,
@ -13,14 +13,14 @@ export default function Ipv6NetworkCard({
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
IPv6 Information
{m.ipv6_information()}
</h3>
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
{networkState?.ipv6_link_local && (
<div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Link-local
{m.ipv6_link_local()}
</span>
<span className="text-sm font-medium">
{networkState?.ipv6_link_local}
@ -42,7 +42,7 @@ export default function Ipv6NetworkCard({
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<div className="col-span-2 flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Address
{m.ipv6_address_label()}
</span>
<span className="text-sm font-medium">{addr.address}</span>
</div>
@ -50,12 +50,12 @@ export default function Ipv6NetworkCard({
{addr.valid_lifetime && (
<div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Valid Lifetime
{m.ipv6_valid_lifetime()}
</span>
<span className="text-sm font-medium">
{addr.valid_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600">
N/A
{m.not_available()}
</span>
) : (
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
@ -66,12 +66,12 @@ export default function Ipv6NetworkCard({
{addr.preferred_lifetime && (
<div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Preferred Lifetime
{m.ipv6_preferred_lifetime()}
</span>
<span className="text-sm font-medium">
{addr.preferred_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600">
N/A
{m.not_available()}
</span>
) : (
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />

View File

@ -1,11 +1,11 @@
import { useEffect, useMemo, useState } from "react";
import { LuExternalLink } from "react-icons/lu";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button, LinkButton } from "@components/Button";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { InputFieldWithLabel } from "./InputField";
import { SelectMenuBasic } from "./SelectMenuBasic";
import { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { m } from "@localizations/messages.js";
export interface JigglerConfig {
inactivity_limit_seconds: number;
@ -51,7 +51,7 @@ export function JigglerSetting({
const exampleConfigs = [
{
name: "Business Hours 9-17",
name: m.jiggler_example_business_hours_late(),
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
@ -60,7 +60,7 @@ export function JigglerSetting({
},
},
{
name: "Business Hours 8-17",
name: m.jiggler_example_business_hours_early(),
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
@ -69,13 +69,10 @@ export function JigglerSetting({
},
},
];
return (
<div className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
Examples
</h4>
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{m.jiggler_examples_label()}</h4>
<div className="flex flex-wrap gap-2">
{exampleConfigs.map((example, index) => (
<Button
@ -90,7 +87,7 @@ export function JigglerSetting({
to="https://crontab.guru/examples.html"
size="XS"
theme="light"
text="More examples"
text={m.jiggler_more_examples()}
LeadingIcon={LuExternalLink}
/>
</div>
@ -100,8 +97,8 @@ export function JigglerSetting({
<InputFieldWithLabel
required
size="SM"
label="Cron Schedule"
description="Cron expression for scheduling"
label={m.jiggler_cron_schedule_label()}
description={m.jiggler_cron_schedule_description()}
placeholder="*/20 * * * * *"
value={jigglerConfigState.schedule_cron_tab}
onChange={e =>
@ -114,8 +111,8 @@ export function JigglerSetting({
<InputFieldWithLabel
size="SM"
label="Inactivity Limit Seconds"
description="Inactivity time before jiggle"
label={m.jiggler_inactivity_limit_label()}
description={m.jiggler_inactivity_limit_description()}
value={jigglerConfigState.inactivity_limit_seconds}
type="number"
min="1"
@ -131,8 +128,8 @@ export function JigglerSetting({
<InputFieldWithLabel
required
size="SM"
label="Random delay"
description="To avoid recognizable patterns"
label={m.jiggler_random_delay_label()}
description={m.jiggler_random_delay_description()}
placeholder="25"
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
value={jigglerConfigState.jitter_percentage}
@ -149,8 +146,8 @@ export function JigglerSetting({
<SelectMenuBasic
size="SM"
label="Timezone"
description="Timezone for cron schedule"
label={m.jiggler_timezone_label()}
description={m.jiggler_timezone_description()}
value={jigglerConfigState.timezone || "UTC"}
disabled={timezones.length === 0}
onChange={e =>
@ -167,7 +164,7 @@ export function JigglerSetting({
<Button
size="SM"
theme="primary"
text="Save Jiggler Config"
text={m.jiggler_save_jiggler_config()}
onClick={() => onSave(jigglerConfigState)}
/>
</div>

View File

@ -1,10 +1,11 @@
import { Link } from "react-router";
import { MdConnectWithoutContact } from "react-icons/md";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { Link } from "react-router";
import { LuEllipsisVertical } from "react-icons/lu";
import Card from "@components/Card";
import { Button, LinkButton } from "@components/Button";
import { m } from "@localizations/messages.js";
function getRelativeTimeString(date: Date | number, lang = navigator.language): string {
// Allow dates or times to be passed
@ -62,16 +63,16 @@ export default function KvmCard({
{online ? (
<div className="flex items-center gap-x-1.5">
<div className="h-2.5 w-2.5 rounded-full border border-green-600 bg-green-500" />
<div className="text-sm text-black dark:text-white">Online</div>
<div className="text-sm text-black dark:text-white">{m.online()}</div>
</div>
) : (
<div className="flex items-center gap-x-1.5">
<div className="h-2.5 w-2.5 rounded-full border border-slate-400/60 dark:border-slate-500 bg-slate-200 dark:bg-slate-600" />
<div className="text-sm text-black dark:text-white">
{lastSeen ? (
<>Last online {getRelativeTimeString(lastSeen)}</>
<>{m.last_online({ time: getRelativeTimeString(lastSeen) })}</>
) : (
<>Never seen online</>
<>{m.never_seen_online()}</>
)}
</div>
</div>
@ -85,7 +86,7 @@ export default function KvmCard({
<LinkButton
size="MD"
theme="light"
text="Connect to KVM"
text={m.connect_to_kvm()}
LeadingIcon={MdConnectWithoutContact}
textAlign="center"
to={`/devices/${id}`}
@ -94,7 +95,7 @@ export default function KvmCard({
<Button
size="MD"
theme="light"
text="Troubleshoot Connection"
text={m.troubleshoot_connection()}
textAlign="center"
/>
)}

View File

@ -1,11 +1,11 @@
import { useEffect } from "react";
import { LuCommand } from "react-icons/lu";
import { useMacrosStore } from "@hooks/stores";
import useKeyboard from "@hooks/useKeyboard";
import { useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button";
import Container from "@components/Container";
import { useMacrosStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import { useJsonRpc } from "@/hooks/useJsonRpc";
export default function MacroBar() {
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();

View File

@ -1,18 +1,19 @@
import { useState } from "react";
import { LuPlus } from "react-icons/lu";
import { Button } from "@/components/Button";
import FieldLabel from "@/components/FieldLabel";
import Fieldset from "@/components/Fieldset";
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
import { MacroStepCard } from "@/components/MacroStepCard";
import { KeySequence } from "@hooks/stores";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
import { Button } from "@components/Button";
import FieldLabel from "@components/FieldLabel";
import Fieldset from "@components/Fieldset";
import { InputFieldWithLabel, FieldError } from "@components/InputField";
import { MacroStepCard } from "@components/MacroStepCard";
import {
DEFAULT_DELAY,
MAX_STEPS_PER_MACRO,
MAX_KEYS_PER_STEP,
} from "@/constants/macros";
import { KeySequence } from "@/hooks/stores";
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import { m } from "@localizations/messages.js";
interface ValidationErrors {
name?: string;
@ -31,7 +32,6 @@ interface MacroFormProps {
onSubmit: (macro: Partial<KeySequence>) => Promise<void>;
onCancel: () => void;
isSubmitting?: boolean;
submitText?: string;
}
export function MacroForm({
@ -39,7 +39,6 @@ export function MacroForm({
onSubmit,
onCancel,
isSubmitting = false,
submitText = "Save Macro",
}: MacroFormProps) {
const [macro, setMacro] = useState<Partial<KeySequence>>(initialData);
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
@ -57,13 +56,13 @@ export function MacroForm({
// Name validation
if (!macro.name?.trim()) {
newErrors.name = "Name is required";
newErrors.name = m.macro_name_required();
} else if (macro.name.trim().length > 50) {
newErrors.name = "Name must be less than 50 characters";
newErrors.name = m.macro_name_too_long();
}
if (!macro.steps?.length) {
newErrors.steps = { 0: { keys: "At least one step is required" } };
newErrors.steps = { 0: { keys: m.macro_at_least_one_step_required() } };
} else {
const hasKeyOrModifier = macro.steps.some(
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
@ -71,7 +70,7 @@ export function MacroForm({
if (!hasKeyOrModifier) {
newErrors.steps = {
0: { keys: "At least one step must have keys or modifiers" },
0: { keys: m.macro_at_least_one_step_keys_or_modifiers() },
};
}
}
@ -82,7 +81,7 @@ export function MacroForm({
const handleSubmit = async () => {
if (!validateForm()) {
showTemporaryError("Please fix the validation errors");
showTemporaryError(m.macro_please_fix_validation_errors());
return;
}
@ -92,7 +91,7 @@ export function MacroForm({
if (error instanceof Error) {
showTemporaryError(error.message);
} else {
showTemporaryError("An error occurred while saving");
showTemporaryError(m.macro_save_error());
}
}
};
@ -114,7 +113,7 @@ export function MacroForm({
? newSteps[stepIndex].keys
: [];
if (keysArray.length >= MAX_KEYS_PER_STEP) {
showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`);
showTemporaryError(m.macro_max_steps_error({max: MAX_KEYS_PER_STEP}));
return;
}
newSteps[stepIndex].keys = [...keysArray, option.value];
@ -178,8 +177,8 @@ export function MacroForm({
<Fieldset>
<InputFieldWithLabel
type="text"
label="Macro Name"
placeholder="Macro Name"
label={m.macro_name_label()}
placeholder={m.macro_name_label()}
value={macro.name}
error={errors.name}
onChange={e => {
@ -197,12 +196,12 @@ export function MacroForm({
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1">
<FieldLabel
label="Steps"
description={`Keys/modifiers executed in sequence with a delay between each step.`}
label={m.macro_steps_label()}
description={m.macro_steps_description()}
/>
</div>
<span className="text-slate-500 dark:text-slate-400">
{macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps
{m.macro_step_count({steps: macro.steps?.length || 0, max: MAX_STEPS_PER_MACRO})}
</span>
</div>
{errors.steps && errors.steps[0]?.keys && (
@ -248,12 +247,10 @@ export function MacroForm({
theme="light"
fullWidth
LeadingIcon={LuPlus}
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ""}`}
text={m.macro_add_step({ maxed_out: isMaxStepsReached ? m.macro_max_steps_reached({ max: MAX_STEPS_PER_MACRO} ) : ""})}
onClick={() => {
if (isMaxStepsReached) {
showTemporaryError(
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
);
showTemporaryError(m.macro_max_steps_error({max: MAX_STEPS_PER_MACRO}));
return;
}
@ -280,11 +277,11 @@ export function MacroForm({
<Button
size="SM"
theme="primary"
text={isSubmitting ? "Saving..." : submitText}
text={isSubmitting ? m.saving() : m.macro_save()}
onClick={handleSubmit}
disabled={isSubmitting}
/>
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
</div>
</div>
</div>

View File

@ -1,14 +1,15 @@
import { useMemo } from "react";
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
import { Button } from "@/components/Button";
import { Combobox } from "@/components/Combobox";
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import Card from "@/components/Card";
import FieldLabel from "@/components/FieldLabel";
import { Button } from "@components/Button";
import { Combobox } from "@components/Combobox";
import Card from "@components/Card";
import FieldLabel from "@components/FieldLabel";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
import { KeyboardLayout } from "@/keyboardLayouts";
import { keys, modifiers } from "@/keyboardMappings";
import { m } from "@localizations/messages.js";
// Filter out modifier keys since they're handled in the modifiers section
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
@ -25,6 +26,7 @@ const groupedModifiers: Record<string, typeof modifierOptions> = {
Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')),
};
// not going to localize these since they're short time intervals
const basePresetDelays = [
{ value: "50", label: "50ms" },
{ value: "100", label: "100ms" },
@ -137,7 +139,7 @@ export function MacroStepCard({
size="XS"
theme="light"
className="text-red-500 dark:text-red-400"
text="Delete"
text={m.delete()}
LeadingIcon={LuTrash2}
onClick={onDelete}
/>
@ -147,7 +149,7 @@ export function MacroStepCard({
<div className="space-y-4 mt-2">
<div className="w-full flex flex-col gap-2">
<FieldLabel label="Modifiers" />
<FieldLabel label={m.macro_step_modifiers_label()} description={m.macro_step_modifiers_description()}/>
<div className="inline-flex flex-wrap gap-3">
{Object.entries(groupedModifiers).map(([group, mods]) => (
<div key={group} className="relative min-w-[120px] rounded-md border border-slate-200 dark:border-slate-700 p-2">
@ -179,7 +181,7 @@ export function MacroStepCard({
<div className="w-full flex flex-col gap-1">
<div className="flex items-center gap-1">
<FieldLabel label="Keys" description={`Maximum ${MAX_KEYS_PER_STEP} keys per step.`} />
<FieldLabel label={m.macro_step_keys_label()} description={m.macro_step_keys_description({max: MAX_KEYS_PER_STEP})} />
</div>
{ensureArray(step.keys) && step.keys.length > 0 && (
<div className="flex flex-wrap gap-1 pb-2">
@ -214,19 +216,19 @@ export function MacroStepCard({
displayValue={() => keyQuery}
onInputChange={onKeyQueryChange}
options={() => filteredKeys}
disabledMessage="Max keys reached"
disabledMessage={m.macro_step_max_keys_reached({max: MAX_KEYS_PER_STEP})}
size="SM"
immediate
disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP}
placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? "Max keys reached" : "Search for key..."}
emptyMessage="No matching keys found"
placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? m.macro_step_max_keys_reached() : m.macro_step_search_for_key()}
emptyMessage={m.macro_step_no_matching_keys_found()}
/>
</div>
</div>
<div className="w-full flex flex-col gap-1">
<div className="flex items-center gap-1">
<FieldLabel label="Step Duration" description="Time to wait before executing the next step." />
<FieldLabel label={m.macro_step_duration_label()} description={m.macro_step_duration_description()} />
</div>
<div className="flex items-center gap-3">
<SelectMenuBasic

View File

@ -2,10 +2,10 @@
import { ComponentProps } from "react";
import { cva, cx } from "cva";
import { someIterable } from "../utils";
import { GridCard } from "./Card";
import MetricsChart from "./MetricsChart";
import { GridCard } from "@components/Card";
import MetricsChart from "@components/MetricsChart";
import { someIterable } from "@/utils";
import { m } from "@localizations/messages.js";
interface ChartPoint {
date: number;
@ -159,7 +159,7 @@ export function Metric<T, K extends keyof T>({
>
{!ready ? (
<div className="flex flex-col items-center space-y-1">
<p className="text-slate-700">Waiting for data...</p>
<p className="text-slate-700">{m.metric_waiting_for_data()}</p>
</div>
) : supportedFinal ? (
<MetricsChart
@ -170,7 +170,7 @@ export function Metric<T, K extends keyof T>({
/>
) : (
<div className="flex flex-col items-center space-y-1">
<p className="text-black">Metric not supported</p>
<p className="text-black">{m.metric_not_supported()}</p>
</div>
)}
</div>

View File

@ -51,6 +51,7 @@ export default function MetricsChart({
axisLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
tickLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
tickFormatter={date => {
// TODO use locale from user settings for date formatting
return new Date(date * 1000).toLocaleString("en-US", {
hourCycle: "h23",
hour: "numeric",

View File

@ -1,6 +1,7 @@
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import EmptyCard from "@/components/EmptyCard";
import EmptyCard from "@components/EmptyCard";
import { m } from "@localizations/messages.js";
export default function NotFoundPage() {
return (
@ -9,8 +10,8 @@ export default function NotFoundPage() {
<div className="w-full max-w-2xl">
<EmptyCard
IconElm={ExclamationTriangleIcon}
headline="Not found"
description="The page you were looking for does not exist."
headline={m.not_found()}
description={m.page_not_found_description()}
/>
</div>
</div>

View File

@ -1,14 +1,15 @@
import StatusCard from "@components/StatusCards";
import { m } from "@localizations/messages.js";
const PeerConnectionStatusMap = {
connected: "Connected",
connecting: "Connecting",
disconnected: "Disconnected",
error: "Connection error",
closing: "Closing",
failed: "Connection failed",
closed: "Closed",
new: "Connecting",
connected: m.peer_connection_connected(),
connecting: m.peer_connection_connecting(),
disconnected: m.peer_connection_disconnected(),
error: m.peer_connection_error(),
closing: m.peer_connection_closing(),
failed: m.peer_connection_failed(),
closed: m.peer_connection_closed(),
new: m.peer_connection_new(),
} as Record<RTCPeerConnectionState | "error" | "closing", string>;
export type PeerConnections = keyof typeof PeerConnectionStatusMap;

View File

@ -1,12 +1,10 @@
import React, { JSX } from "react";
import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel";
import Card from "@components/Card";
import FieldLabel from "@components/FieldLabel";
import { cva } from "@/cva.config";
import Card from "./Card";
type SelectMenuProps = Pick<
JSX.IntrinsicElements["select"],
"disabled" | "onChange" | "name" | "value"

View File

@ -1,6 +1,7 @@
import { AvailableSidebarViews } from "@hooks/stores";
import { Button } from "@components/Button";
import { cx } from "@/cva.config";
import { AvailableSidebarViews } from "@/hooks/stores";
import { m } from "@localizations/messages.js";
export default function SidebarHeader({
title,
@ -17,7 +18,7 @@ export default function SidebarHeader({
<Button
size="XS"
theme="blank"
text="Hide"
text={m.hide()}
LeadingIcon={({ className }) => (
<svg
className={cx(className, "rotate-180")}

View File

@ -1,9 +1,9 @@
import { Link } from "react-router";
import React from "react";
import { Link } from "react-router";
import Container from "@/components/Container";
import LogoBlueIcon from "@/assets/logo-blue.png";
import LogoWhiteIcon from "@/assets/logo-white.svg";
import LogoBlueIcon from "@assets/logo-blue.png";
import LogoWhiteIcon from "@assets/logo-white.svg";
import Container from "@components/Container";
interface Props { logoHref?: string; actionElement?: React.ReactNode }

View File

@ -1,7 +1,8 @@
import { CheckIcon } from "@heroicons/react/16/solid";
import Card from "@components/Card";
import { m } from "@localizations/messages.js";
import { cva, cx } from "@/cva.config";
import Card from "@/components/Card";
interface Props {
nSteps: number;
@ -49,7 +50,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
)}
key={`${i}-${currStepIdx}`}
>
Step {i + 1}
{m.step_counter_step({ step: i + 1 })}
</div>
);
}

View File

@ -1,17 +1,17 @@
import { useEffect, useMemo } from "react";
import "react-simple-keyboard/build/css/index.css";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { useEffect, useMemo } from "react";
import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { WebglAddon } from "@xterm/addon-webgl";
import { Unicode11Addon } from "@xterm/addon-unicode11";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { cx } from "@/cva.config";
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
import { Button } from "./Button";
import { AvailableTerminalTypes, useUiStore } from "@hooks/stores";
import { Button } from "@components/Button";
import { m } from "@localizations/messages.js";
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
@ -191,7 +191,7 @@ function Terminal({
<Button
size="XS"
theme="light"
text="Hide"
text={m.hide()}
LeadingIcon={ChevronDownIcon}
onClick={() => setTerminalType("none")}
/>

View File

@ -1,9 +1,9 @@
import React, { JSX } from "react";
import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel";
import { FieldError } from "@/components/InputField";
import Card from "@/components/Card";
import FieldLabel from "@components/FieldLabel";
import { FieldError } from "@components/InputField";
import Card from "@components/Card";
import { cx } from "@/cva.config";
type TextAreaProps = JSX.IntrinsicElements["textarea"] & {

View File

@ -1,10 +1,12 @@
import React from "react";
import { cx } from "@/cva.config";
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
import KeyboardAndMouseConnectedIcon from "@assets/keyboard-and-mouse-connected.png";
import { USBStates } from "@hooks/stores";
import { m } from "@localizations/messages.js";
import LoadingSpinner from "@components/LoadingSpinner";
import StatusCard from "@components/StatusCards";
import { USBStates } from "@/hooks/stores";
type StatusProps = Record<
USBStates,
@ -16,11 +18,11 @@ type StatusProps = Record<
>;
const USBStateMap: Record<USBStates, string> = {
configured: "Connected",
attached: "Connecting",
addressed: "Connecting",
"not attached": "Disconnected",
suspended: "Low power mode",
configured: m.usb_state_connected(),
attached: m.usb_state_connecting(),
addressed: m.usb_state_connecting(),
"not attached": m.usb_state_disconnected(),
suspended: m.usb_state_low_power_mode(),
};
const StatusCardProps: StatusProps = {
configured: {
@ -80,8 +82,8 @@ export default function USBStateStatus({
return (
<StatusCard
title="USB"
status="Disconnected"
title={m.usb()}
status={m.usb_state_disconnected()}
icon={Icon}
iconClassName={iconClassName}
statusIndicatorClassName={statusIndicatorClassName}
@ -90,6 +92,6 @@ export default function USBStateStatus({
}
return (
<StatusCard title="USB" status={USBStateMap[state]} {...StatusCardProps[state]} />
<StatusCard title={m.usb()} status={USBStateMap[state]} {...StatusCardProps[state]} />
);
}

View File

@ -1,10 +1,10 @@
import { cx } from "@/cva.config";
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
import { Button } from "./Button";
import { GridCard } from "./Card";
import LoadingSpinner from "./LoadingSpinner";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
import LoadingSpinner from "@components/LoadingSpinner";
import { m } from "@localizations/messages.js";
export default function UpdateInProgressStatusCard() {
const { navigateTo } = useDeviceUiNavigation();
@ -17,12 +17,12 @@ export default function UpdateInProgressStatusCard() {
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
<div className="space-y-1">
<div className="text-ellipsis text-sm font-semibold leading-none transition">
Update in Progress
{m.update_in_progress()}
</div>
<div className="text-sm leading-none">
<div className="flex items-center gap-x-1">
<span className={cx("transition")}>
Please don{"'"}t turn off your device...
{m.updating_leave_device_on()}
</span>
</div>
</div>
@ -32,7 +32,7 @@ export default function UpdateInProgressStatusCard() {
size="SM"
className="pointer-events-auto"
theme="light"
text="View Details"
text={m.view_details()}
onClick={() => navigateTo("/settings/general/update")}
/>
</div>

View File

@ -1,15 +1,15 @@
import { useCallback, useEffect, useState } from "react";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { m } from "@localizations/messages.js";
import { SettingsItem } from "@components/SettingsItem";
import Checkbox from "@components/Checkbox";
import { Button } from "@components/Button";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import Fieldset from "@components/Fieldset";
import notifications from "@/notifications";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import Checkbox from "./Checkbox";
import { Button } from "./Button";
import { SelectMenuBasic } from "./SelectMenuBasic";
import { SettingsSectionHeader } from "./SettingsSectionHeader";
import Fieldset from "./Fieldset";
export interface USBConfig {
vendor_id: string;
product_id: string;
@ -34,7 +34,7 @@ const defaultUsbDeviceConfig: UsbDeviceConfig = {
const usbPresets = [
{
label: "Keyboard, Mouse and Mass Storage",
label: m.usb_device_keyboard_mouse_and_mass_storage(),
value: "default",
config: {
keyboard: true,
@ -44,7 +44,7 @@ const usbPresets = [
},
},
{
label: "Keyboard Only",
label: m.usb_device_keyboard_only(),
value: "keyboard_only",
config: {
keyboard: true,
@ -54,7 +54,7 @@ const usbPresets = [
},
},
{
label: "Custom",
label: m.usb_device_custom(),
value: "custom",
},
];
@ -72,7 +72,7 @@ export function UsbDeviceSetting() {
if ("error" in resp) {
console.error("Failed to load USB devices:", resp.error);
notifications.error(
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`,
m.usb_device_failed_load({ error: String(resp.error.data || "Unknown error") }),
);
} else {
const usbConfigState = resp.result as UsbDeviceConfig;
@ -101,7 +101,7 @@ export function UsbDeviceSetting() {
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
`Failed to set usb devices: ${resp.error.data || "Unknown error"}`,
m.usb_device_failed_set({ error: String(resp.error.data || "Unknown error") }),
);
setLoading(false);
return;
@ -111,7 +111,7 @@ export function UsbDeviceSetting() {
await new Promise(resolve => setTimeout(resolve, 2000));
setLoading(false);
syncUsbDeviceConfig();
notifications.success(`USB Devices updated`);
notifications.success(m.usb_device_updated());
});
},
[send, syncUsbDeviceConfig],
@ -154,14 +154,14 @@ export function UsbDeviceSetting() {
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
<SettingsSectionHeader
title="USB Device"
description="USB devices to emulate on the target computer"
title={m.usb_device_title()}
description={m.usb_device_description()}
/>
<SettingsItem
loading={loading}
title="Classes"
description="USB device classes in the composite device"
title={m.usb_device_classes_title()}
description={m.usb_device_classes_description()}
>
<SelectMenuBasic
size="SM"
@ -178,7 +178,7 @@ export function UsbDeviceSetting() {
<div className="ml-2 border-l border-slate-800/10 pl-4 dark:border-slate-300/20 ">
<div className="space-y-4">
<div className="space-y-4">
<SettingsItem title="Enable Keyboard" description="Enable Keyboard">
<SettingsItem title={m.usb_device_enable_keyboard_title()} description={m.usb_device_enable_keyboard_description()}>
<Checkbox
checked={usbDeviceConfig.keyboard}
onChange={onUsbConfigItemChange("keyboard")}
@ -187,8 +187,8 @@ export function UsbDeviceSetting() {
</div>
<div className="space-y-4">
<SettingsItem
title="Enable Absolute Mouse (Pointer)"
description="Enable Absolute Mouse (Pointer)"
title={m.usb_device_enable_absolute_mouse_title()}
description={m.usb_device_enable_absolute_mouse_description()}
>
<Checkbox
checked={usbDeviceConfig.absolute_mouse}
@ -198,8 +198,8 @@ export function UsbDeviceSetting() {
</div>
<div className="space-y-4">
<SettingsItem
title="Enable Relative Mouse"
description="Enable Relative Mouse"
title={m.usb_device_enable_relative_mouse_title()}
description={m.usb_device_enable_relative_mouse_description()}
>
<Checkbox
checked={usbDeviceConfig.relative_mouse}
@ -209,8 +209,8 @@ export function UsbDeviceSetting() {
</div>
<div className="space-y-4">
<SettingsItem
title="Enable USB Mass Storage"
description="Sometimes it might need to be disabled to prevent issues with certain devices"
title={m.usb_device_enable_mass_storage_title()}
description={m.usb_device_enable_mass_storage_description()}
>
<Checkbox
checked={usbDeviceConfig.mass_storage}
@ -224,13 +224,13 @@ export function UsbDeviceSetting() {
size="SM"
loading={loading}
theme="primary"
text="Update USB Classes"
text={m.usb_device_update_classes()}
onClick={() => handleUsbConfigChange(usbDeviceConfig)}
/>
<Button
size="SM"
theme="light"
text="Restore to Default"
text={m.usb_device_restore_default()}
onClick={() => handleUsbConfigChange(defaultUsbDeviceConfig)}
/>
</div>

View File

@ -1,15 +1,14 @@
import { useMemo , useCallback , useEffect, useState } from "react";
import { UsbConfigState } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button";
import Fieldset from "@components/Fieldset";
import { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem";
import { UsbConfigState } from "../hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { InputFieldWithLabel } from "./InputField";
import { SelectMenuBasic } from "./SelectMenuBasic";
import Fieldset from "./Fieldset";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&");
@ -31,21 +30,22 @@ export interface USBConfig {
product: string;
}
const usbConfigs = [
{
label: "JetKVM Default",
label: m.usb_config_default(),
value: "USB Emulation Device",
},
{
label: "Logitech Universal Adapter",
label: m.usb_config_logitech(),
value: "Logitech USB Input Device",
},
{
label: "Microsoft Wireless MultiMedia Keyboard",
label: m.usb_config_microsoft(),
value: "Wireless MultiMedia Keyboard",
},
{
label: "Dell Multimedia Pro Keyboard",
label: m.usb_config_dell(),
value: "Multimedia Pro Keyboard",
},
];
@ -97,7 +97,7 @@ export function UsbInfoSetting() {
if ("error" in resp) {
console.error("Failed to load USB Config:", resp.error);
notifications.error(
`Failed to load USB Config: ${resp.error.data || "Unknown error"}`,
m.usb_config_failed_load({ error: String(resp.error.data || "Unknown error") }),
);
} else {
const usbConfigState = resp.result as UsbConfigState;
@ -116,7 +116,7 @@ export function UsbInfoSetting() {
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
`Failed to set usb config: ${resp.error.data || "Unknown error"}`,
m.usb_config_failed_set({ error: String(resp.error.data || "Unknown error") }),
);
setLoading(false);
return;
@ -126,7 +126,7 @@ export function UsbInfoSetting() {
await new Promise(resolve => setTimeout(resolve, 2000));
setLoading(false);
notifications.success(
`USB Config set to ${usbConfig.manufacturer} ${usbConfig.product}`,
m.usb_config_set_success({ manufacturer: usbConfig.manufacturer, product: usbConfig.product }),
);
syncUsbConfigProduct();
@ -152,8 +152,8 @@ export function UsbInfoSetting() {
<Fieldset disabled={loading} className="space-y-4">
<SettingsItem
loading={loading}
title="Identifiers"
description="USB device identifiers exposed to the target computer"
title={m.usb_config_identifiers_title()}
description={m.usb_config_identifiers_description()}
>
<SelectMenuBasic
size="SM"
@ -169,7 +169,7 @@ export function UsbInfoSetting() {
handleUsbConfigChange(usbConfig);
}
}}
options={[...usbConfigs, { value: "custom", label: "Custom" }]}
options={[...usbConfigs, { value: "custom", label: m.usb_config_custom() }]}
/>
</SettingsItem>
{usbConfigProduct === "custom" && (
@ -246,38 +246,38 @@ function USBConfigDialog({
<div className="grid grid-cols-2 gap-4">
<InputFieldWithLabel
required
label="Vendor ID"
placeholder="Enter Vendor ID"
label={m.usb_config_vendor_id_label()}
placeholder={m.usb_config_vendor_id_placeholder()}
pattern="^0[xX][\da-fA-F]{4}$"
defaultValue={usbConfigState?.vendor_id}
onChange={e => handleUsbVendorIdChange(e.target.value)}
/>
<InputFieldWithLabel
required
label="Product ID"
placeholder="Enter Product ID"
label={m.usb_config_product_id_label()}
placeholder={m.usb_config_product_id_placeholder()}
pattern="^0[xX][\da-fA-F]{4}$"
defaultValue={usbConfigState?.product_id}
onChange={e => handleUsbProductIdChange(e.target.value)}
/>
<InputFieldWithLabel
required
label="Serial Number"
placeholder="Enter Serial Number"
label={m.usb_config_serial_number_label()}
placeholder={m.usb_config_serial_number_placeholder()}
defaultValue={usbConfigState?.serial_number}
onChange={e => handleUsbSerialChange(e.target.value)}
/>
<InputFieldWithLabel
required
label="Manufacturer"
placeholder="Enter Manufacturer"
label={m.usb_config_manufacturer_label()}
placeholder={m.usb_config_manufacturer_placeholder()}
defaultValue={usbConfigState?.manufacturer}
onChange={e => handleUsbManufacturer(e.target.value)}
/>
<InputFieldWithLabel
required
label="Product Name"
placeholder="Enter Product Name"
label={m.usb_config_product_name_label()}
placeholder={m.usb_config_product_name_placeholder()}
defaultValue={usbConfigState?.product}
onChange={e => handleUsbProduct(e.target.value)}
/>
@ -287,13 +287,13 @@ function USBConfigDialog({
loading={loading}
size="SM"
theme="primary"
text="Update USB Identifiers"
text={m.usb_config_update_identifiers()}
onClick={() => onSetUsbConfig(usbConfigState)}
/>
<Button
size="SM"
theme="light"
text="Restore to Default"
text={m.usb_config_restore_default()}
onClick={onRestoreToDefault}
/>
</div>

View File

@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "framer-motion";
import { LuPlay } from "react-icons/lu";
import { BsMouseFill } from "react-icons/bs";
import { m } from "@localizations/messages.js";
import { Button, LinkButton } from "@components/Button";
import LoadingSpinner from "@components/LoadingSpinner";
import Card, { GridCard } from "@components/Card";
@ -46,7 +47,7 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
<LoadingSpinner className="h-8 w-8 text-blue-800 dark:text-blue-200" />
</div>
<p className="text-center text-sm text-slate-700 dark:text-slate-300">
Loading video stream...
{m.video_overlay_loading_stream()}
</p>
</div>
</OverlayContent>
@ -118,26 +119,26 @@ export function ConnectionFailedOverlay({
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4">
<div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">Connection Issue Detected</h2>
<h2 className="text-xl font-bold">{m.video_overlay_connection_issue_title()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left">
<li>Verify that the device is powered on and properly connected</li>
<li>Check all cable connections for any loose or damaged wires</li>
<li>Ensure your network connection is stable and active</li>
<li>Try restarting both the device and your computer</li>
<li>{m.video_overlay_conn_verify_power()}</li>
<li>{m.video_overlay_conn_check_cables()}</li>
<li>{m.video_overlay_conn_ensure_network()}</li>
<li>{m.video_overlay_conn_restart()}</li>
</ul>
</div>
<div className="flex items-center gap-x-2">
<LinkButton
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
theme="primary"
text="Troubleshooting Guide"
text={m.video_overlay_troubleshooting_guide()}
TrailingIcon={ArrowRightIcon}
size="SM"
/>
<Button
onClick={() => setupPeerConnection()}
LeadingIcon={ArrowPathIcon}
text="Try again"
text={m.video_overlay_try_again()}
size="SM"
theme="light"
/>
@ -178,12 +179,12 @@ export function PeerConnectionDisconnectedOverlay({
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4">
<div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">Connection Issue Detected</h2>
<h2 className="text-xl font-bold">{m.video_overlay_connection_issue_title()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left">
<li>Verify that the device is powered on and properly connected</li>
<li>Check all cable connections for any loose or damaged wires</li>
<li>Ensure your network connection is stable and active</li>
<li>Try restarting both the device and your computer</li>
<li>{m.video_overlay_conn_verify_power()}</li>
<li>{m.video_overlay_conn_check_cables()}</li>
<li>{m.video_overlay_conn_ensure_network()}</li>
<li>{m.video_overlay_conn_restart()}</li>
</ul>
</div>
<div className="flex items-center gap-x-2">
@ -191,7 +192,7 @@ export function PeerConnectionDisconnectedOverlay({
<div className="flex items-center gap-x-2 p-4">
<LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" />
<p className="text-sm text-slate-700 dark:text-slate-300">
Retrying connection...
{m.video_overlay_retrying_connection()}
</p>
</div>
</Card>
@ -235,23 +236,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4">
<div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">No HDMI signal detected.</h2>
<h2 className="text-xl font-bold">{m.video_overlay_no_hdmi_signal()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left">
<li>Ensure the HDMI cable securely connected at both ends</li>
<li>
Ensure source device is powered on and outputting a signal
</li>
<li>
If using an adapter, ensure it&apos;s compatible and functioning
correctly
</li>
<li>{m.video_overlay_no_hdmi_ensure_cable()}</li>
<li>{m.video_overlay_no_hdmi_ensure_power()}</li>
<li>{m.video_overlay_no_hdmi_adapter_compat()}</li>
</ul>
</div>
<div>
<LinkButton
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
theme="light"
text="Learn more"
text={m.video_overlay_learn_more()}
TrailingIcon={ArrowRightIcon}
size="SM"
/>
@ -282,18 +278,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4">
<div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">HDMI signal error detected.</h2>
<h2 className="text-xl font-bold">{m.video_overlay_hdmi_error_title()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left">
<li>A loose or faulty HDMI connection</li>
<li>Incompatible resolution or refresh rate settings</li>
<li>Issues with the source device&apos;s HDMI output</li>
<li>{m.video_overlay_hdmi_loose_faulty()}</li>
<li>{m.video_overlay_hdmi_incompatible_resolution()}</li>
<li>{m.video_overlay_hdmi_source_issue()}</li>
</ul>
</div>
<div>
<LinkButton
to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
theme="light"
text="Learn more"
text={m.video_overlay_learn_more()}
TrailingIcon={ArrowRightIcon}
size="SM"
/>
@ -334,7 +330,7 @@ export function NoAutoplayPermissionsOverlay({
<OverlayContent>
<div className="space-y-4">
<h2 className="text-2xl font-extrabold text-black dark:text-white">
Autoplay permissions required
{m.video_overlay_autoplay_permissions_required()}
</h2>
<div className="space-y-2 text-center">
@ -343,13 +339,13 @@ export function NoAutoplayPermissionsOverlay({
size="MD"
theme="primary"
LeadingIcon={LuPlay}
text="Manually start stream"
text={m.video_overlay_manually_start_stream()}
onClick={onPlayClick}
/>
</div>
<div className="text-xs text-slate-600 dark:text-slate-400">
Please adjust browser settings to enable autoplay
{m.video_overlay_enable_autoplay_settings()}
</div>
</div>
</div>
@ -381,7 +377,7 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
<div className="flex items-center space-x-2">
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
<span className="text-sm text-black dark:text-white">
Click on the video to enable mouse control
{m.video_overlay_pointerlock_click_to_enable()}
</span>
</div>
</div>

View File

@ -1,21 +1,19 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Keyboard from "react-simple-keyboard";
import { LuKeyboard } from "react-icons/lu";
import Card from "@components/Card";
// eslint-disable-next-line import/order
import { Button, LinkButton } from "@components/Button";
import "react-simple-keyboard/build/css/index.css";
import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config";
import { useHidStore, useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import { useHidStore, useUiStore } from "@hooks/stores";
import useKeyboard from "@hooks/useKeyboard";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
import { Button, LinkButton } from "@components/Button";
import Card from "@components/Card";
import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings";
import { m } from "@localizations/messages.js";
export const DetachIcon = ({ className }: { className?: string }) => {
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
@ -244,20 +242,20 @@ function KeyboardWrapper() {
<Button
size="XS"
theme="light"
text="Detach"
text={m.detach()}
onClick={() => setAttachedVirtualKeyboardVisibility(false)}
/>
) : (
<Button
size="XS"
theme="light"
text="Attach"
text={m.attach()}
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
/>
)}
</div>
<h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300">
Virtual Keyboard
m.virtual_keyboard_header()
</h2>
<div className="absolute right-2 flex items-center gap-x-2">
<div className="hidden md:flex gap-x-2 items-center">
@ -274,7 +272,7 @@ function KeyboardWrapper() {
<Button
size="XS"
theme="light"
text="Hide"
text={m.hide()}
LeadingIcon={ChevronDownIcon}
onClick={() => setVirtualKeyboardEnabled(false)}
/>

View File

@ -1,27 +1,27 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useResizeObserver } from "usehooks-ts";
import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
import MacroBar from "@/components/MacroBar";
import InfoBar from "@components/InfoBar";
import notifications from "@/notifications";
import useKeyboard from "@/hooks/useKeyboard";
import { cx } from "@/cva.config";
import { keys } from "@/keyboardMappings";
import useKeyboard from "@hooks/useKeyboard";
import useMouse from "@hooks/useMouse";
import {
useRTCStore,
useSettingsStore,
useVideoStore,
} from "@/hooks/stores";
import useMouse from "@/hooks/useMouse";
} from "@hooks/stores";
import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
import MacroBar from "@components/MacroBar";
import InfoBar from "@components/InfoBar";
import {
HDMIErrorOverlay,
LoadingVideoOverlay,
NoAutoplayPermissionsOverlay,
PointerLockBar,
} from "./VideoOverlay";
} from "@components/VideoOverlay";
import { keys } from "@/keyboardMappings";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
export default function WebRTCVideo() {
// Video and stream related refs and states
@ -168,10 +168,10 @@ export default function WebRTCVideo() {
const handlePointerLockChange = () => {
if (document.pointerLockElement) {
notifications.success("Pointer lock Enabled, press escape to unlock");
notifications.success(m.video_pointer_lock_enabled());
setIsPointerLockActive(true);
} else {
notifications.success("Pointer lock Disabled");
notifications.success(m.video_pointer_lock_disabled());
setIsPointerLockActive(false);
}
};

View File

@ -106,7 +106,7 @@ export default function PasteModal() {
}
} catch (error) {
console.error("Failed to paste text:", error);
notifications.error("Failed to paste text");
notifications.error(m.paste_modal_failed_paste({ error: String(error) }));
}
}, [selectedKeyboard, executeMacro, delay]);

View File

@ -1,10 +1,10 @@
import { useInterval } from "usehooks-ts";
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";
export default function ConnectionStatsSidebar() {

View File

@ -270,9 +270,8 @@ export class KeyboardMacroReportMessage extends RpcMessage {
...keys,
...fromUint16toUint8(step.delay),
]);
const offset = 6 + i * 9;
data.set(macroBinary, offset);
}

View File

@ -54,7 +54,8 @@ export default function useKeyboard() {
// support the keyPressReport API. In that case, we need to handle the key presses locally
// and send the full state to the device, so it can behave like a real USB HID keyboard.
// This flag indicates whether the keyPressReport API is available on the device which is
// dynamically set when the device responds to the first key press event or reports its // keysDownState when queried since the keyPressReport was introduced together with the
// dynamically set when the device responds to the first key press event or reports its
// keysDownState when queried since the keyPressReport was introduced together with the
// getKeysDownState API.
// HidRPC is a binary format for exchanging keyboard and mouse events
@ -277,7 +278,6 @@ export default function useKeyboard() {
cancelKeepAlive();
}, [cancelKeepAlive]);
// executeMacro is used to execute a macro consisting of multiple steps.
// Each step can have multiple keys, multiple modifiers and a delay.
// The keys and modifiers are pressed together and held for the delay duration.
@ -306,6 +306,7 @@ export default function useKeyboard() {
sendKeyboardMacroEventHidRpc(macro);
}, [sendKeyboardMacroEventHidRpc]);
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
const promises: (() => Promise<void>)[] = [];
@ -355,6 +356,7 @@ export default function useKeyboard() {
});
});
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]);
const executeMacro = useCallback(async (steps: MacroSteps) => {
if (rpcHidReady) {
return executeMacroRemote(steps);

View File

@ -3,6 +3,7 @@ import { useCallback } from "react";
import { useDeviceStore } from "@/hooks/stores";
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
export interface VersionInfo {
appVersion: string;
@ -29,7 +30,7 @@ export function useVersion() {
return new Promise<SystemVersionInfo>((resolve, reject) => {
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(`Failed to check for updates: ${resp.error}`);
notifications.error(m.updates_failed_check({ error: String(resp.error) }));
reject(new Error("Failed to check for updates"));
} else {
const result = resp.result as SystemVersionInfo;
@ -37,7 +38,7 @@ export function useVersion() {
setSystemVersion(result.local.systemVersion);
if (result.error) {
notifications.error(`Failed to check for updates: ${result.error}`);
notifications.error(m.updates_failed_check({ error: String(result.error) }));
reject(new Error("Failed to check for updates"));
} else {
resolve(result);
@ -57,7 +58,7 @@ export function useVersion() {
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
}
console.error("Failed to get device version N", resp.error);
notifications.error(`Failed to get device version: ${resp.error}`);
notifications.error(m.updates_failed_get_device_version({ error: String(resp.error) }));
reject(new Error("Failed to get device version"));
} else {
const result = resp.result as VersionInfo;

View File

@ -113,7 +113,6 @@ export default function SettingsMacrosEditRoute() {
onSubmit={handleUpdateMacro}
onCancel={() => navigate("../")}
isSubmitting={isUpdating}
submitText="Save Changes"
/>
<ConfirmDialog

View File

@ -1,5 +1,6 @@
import { useLocation, useSearchParams } from "react-router";
import { m } from "@localizations/messages.js";
import AuthLayout from "@components/AuthLayout";
export default function LoginRoute() {
@ -11,11 +12,11 @@ export default function LoginRoute() {
return (
<AuthLayout
showCounter={true}
title="Connect your JetKVM to the cloud"
description="Unlock remote access and advanced features for your device"
action="Log in & Connect device"
title={m.auth_connect_to_cloud()}
description={m.auth_connect_to_cloud_description()}
action={m.auth_connect_to_cloud_action()}
// Header CTA
cta="Don't have an account?"
cta={m.auth_header_cta_dont_have_account()}
ctaHref={`/signup?${sq.toString()}`}
/>
);
@ -23,11 +24,11 @@ export default function LoginRoute() {
return (
<AuthLayout
title="Log in to your JetKVM account"
description="Log in to access and manage your devices securely"
action="Log in"
title={m.auth_login()}
description={m.auth_login_description()}
action={m.auth_login_action()}
// Header CTA
cta="New to JetKVM?"
cta={m.auth_header_cta_new_to_jetkvm()}
ctaHref={`/signup?${sq.toString()}`}
/>
);

View File

@ -1,5 +1,6 @@
import { useLocation, useSearchParams } from "react-router";
import { m } from "@localizations/messages.js";
import AuthLayout from "@components/AuthLayout";
export default function SignupRoute() {
@ -11,10 +12,10 @@ export default function SignupRoute() {
return (
<AuthLayout
showCounter={true}
title="Connect your JetKVM to the cloud"
description="Unlock remote access and advanced features for your device."
action="Signup & Connect device"
cta="Already have an account?"
title={m.auth_connect_to_cloud()}
description={m.auth_connect_to_cloud_description()}
action={m.auth_signup_connect_to_cloud_action()}
cta={m.auth_header_cta_already_have_account()}
ctaHref={`/login?${sq.toString()}`}
/>
);
@ -22,11 +23,11 @@ export default function SignupRoute() {
return (
<AuthLayout
title="Create your JetKVM account"
description="Create your account and start managing your devices with ease."
action="Create Account"
title={m.auth_signup_create_account()}
description={m.auth_signup_create_account_description()}
action={m.auth_signup_create_account_action()}
// Header CTA
cta="Already have an account?"
cta={m.auth_header_cta_already_have_account()}
ctaHref={`/login?${sq.toString()}`}
/>
);