mirror of https://github.com/jetkvm/kvm.git
Localize all pages except Settings
This commit is contained in:
parent
ea43caae27
commit
66ab743dfe
|
|
@ -9,8 +9,6 @@ const {
|
|||
fixupConfigRules,
|
||||
} = require("@eslint/compat");
|
||||
|
||||
const tsParser = require("@typescript-eslint/parser");
|
||||
const reactRefresh = require("eslint-plugin-react-refresh");
|
||||
const js = require("@eslint/js");
|
||||
|
||||
const {
|
||||
|
|
@ -23,6 +21,9 @@ const compat = new FlatCompat({
|
|||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
const tsParser = require("@typescript-eslint/parser");
|
||||
const reactRefresh = require("eslint-plugin-react-refresh");
|
||||
|
||||
module.exports = defineConfig([{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,146 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"jetkvm": "JetKVM",
|
||||
"jetkvm_logo": "JetKVM Logo",
|
||||
"action_bar_connection_stats": "Connection Stats",
|
||||
"action_bar_exit_fullscreen": "Exit Fullscreen",
|
||||
"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",
|
||||
"already_adopted_new_owner": "If you're the new owner, please ask the previous owner to de-register the device from their account in the cloud dashboard. If you believe this is an error, contact our support team for assistance.",
|
||||
"already_adopted_other_user": "This device is currently registered to another user in our cloud dashboard.",
|
||||
"already_adopted_return_to_dashboard": "Return to Dashboard",
|
||||
"already_adopted_title": "Device Already Registered",
|
||||
"attach": "Attach",
|
||||
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
|
||||
"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",
|
||||
"auth_authentication_mode_error": "An error occurred while setting the authentication mode",
|
||||
"auth_authentication_mode_invalid": "Invalid authentication mode",
|
||||
"auth_authentication_mode": "Please select an authentication mode",
|
||||
"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_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_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_mode_local_change_later": "You can always change your authentication method later in the settings.",
|
||||
"auth_mode_local_description": "Select how you would like to secure your JetKVM device locally.",
|
||||
"auth_mode_local_no_password_description": "Quick access without password authentication.",
|
||||
"auth_mode_local_no_password": "No Password",
|
||||
"auth_mode_local_password_confirm_description": "Confirm your password",
|
||||
"auth_mode_local_password_confirm_label": "Confirm Password",
|
||||
"auth_mode_local_password_description": "Secure your device with a password for added protection.",
|
||||
"auth_mode_local_password_do_not_match": "Passwords do not match",
|
||||
"auth_mode_local_password_failed_set": "Failed to set password: {error}",
|
||||
"auth_mode_local_password_note_local": "All data remains on your local device.",
|
||||
"auth_mode_local_password_note": "This password will be used to secure your device data and protect against unauthorized access.",
|
||||
"auth_mode_local_password_set_button": "Set Password",
|
||||
"auth_mode_local_password_set_description": "Create a strong password to secure your JetKVM device locally.",
|
||||
"auth_mode_local_password_set_label": "Enter a password",
|
||||
"auth_mode_local_password_set": "Set a Password",
|
||||
"auth_mode_local_password": "Password",
|
||||
"auth_mode_local": "Local Authentication Method",
|
||||
"auth_signup_connect_to_cloud_action": "Signup & Connect device",
|
||||
"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",
|
||||
"back_to_devices": "Back to Devices",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"cloud_kvms_description": "Manage your cloud KVMs and connect to them securely.",
|
||||
"cloud_kvms_no_devices_description": "You don't have any devices with enabled JetKVM Cloud yet.",
|
||||
"cloud_kvms_no_devices": "No devices found",
|
||||
"cloud_kvms": "Cloud KVMs",
|
||||
"confirm": "Confirm",
|
||||
"connect_to_kvm": "Connect to KVM",
|
||||
"connecting_to_device": "Connecting to device…",
|
||||
"connection_established": "Connection established",
|
||||
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
|
||||
"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_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",
|
||||
"continue": "Continue",
|
||||
"creating_peer_connection": "Creating peer connection...",
|
||||
"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_voltage_unit": "V",
|
||||
"dc_power_control_voltage": "Voltage",
|
||||
"default": "Default",
|
||||
"delete": "Delete",
|
||||
"deregister_button": "Deregister from Cloud",
|
||||
"deregister_cloud_devices": "Cloud Devices",
|
||||
"deregister_description": "This will remove the device from your cloud account and revoke remote access to it. Please note that local access will still be possible",
|
||||
"deregister_error": "There was an error {status} deregistering your device. Please try again.",
|
||||
"deregister_from_cloud": "Deregister from cloud",
|
||||
"deregister_headline": "Deregister {device} from your cloud account",
|
||||
"detach": "Detach",
|
||||
"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",
|
||||
"dhcp_server": "DHCP Server",
|
||||
"dns_servers": "DNS Servers",
|
||||
"establishing_secure_connection": "Establishing secure connection…",
|
||||
"experimental": "Experimental",
|
||||
"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_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",
|
||||
"gathering_ice_candidates": "Gathering ICE candidates...",
|
||||
"getting_remote_session_description": "Getting remote session description attempt {attempt}",
|
||||
"hide": "Hide",
|
||||
"ice_gathering_completed": "ICE Gathering completed",
|
||||
"info_caps_lock": "Caps Lock",
|
||||
"info_compose": "Compose",
|
||||
"info_hdmi_state": "HDMI State:",
|
||||
|
|
@ -32,73 +159,186 @@
|
|||
"info_usb_state": "USB State:",
|
||||
"info_video_size": "Video Size:",
|
||||
"input_disabled": "Input disabled",
|
||||
"invalid_password": "Invalid password",
|
||||
"ip_address": "IP Address",
|
||||
"ipv6_address_label": "Address",
|
||||
"ipv6_addresses": "IPv6 Addresses",
|
||||
"ipv6_information": "IPv6 Information",
|
||||
"ipv6_link_local": "Link-local",
|
||||
"ipv6_preferred_lifetime": "Preferred Lifetime",
|
||||
"ipv6_valid_lifetime": "Valid Lifetime",
|
||||
"jetkvm_description": "JetKVM combines powerful hardware with intuitive software to provide a seamless remote control experience.",
|
||||
"jetkvm_device": "JetKVM Device",
|
||||
"jetkvm_logo": "JetKVM Logo",
|
||||
"jetkvm_setup": "Set up your JetKVM",
|
||||
"jetkvm": "JetKVM",
|
||||
"jiggler_cron_schedule_description": "Cron expression for scheduling",
|
||||
"jiggler_cron_schedule_label": "Cron Schedule",
|
||||
"jiggler_example_business_hours_early": "Business Hours 8-17",
|
||||
"jiggler_example_business_hours_late": "Business Hours 9-17",
|
||||
"jiggler_examples_label": "Examples",
|
||||
"jiggler_inactivity_limit_description": "Inactivity time before jiggle",
|
||||
"jiggler_inactivity_limit_label": "Inactivity Limit Seconds",
|
||||
"jiggler_more_examples": "More examples",
|
||||
"jiggler_random_delay_description": "To avoid recognizable patterns",
|
||||
"jiggler_random_delay_label": "Random delay",
|
||||
"jiggler_save_jiggler_config": "Save Jiggler Config",
|
||||
"jiggler_timezone_description": "Timezone for cron schedule",
|
||||
"jiggler_timezone_label": "Timezone",
|
||||
"kvm_terminal": "KVM Terminal",
|
||||
"last_online": "Last online {time}",
|
||||
"learn_more": "Learn more",
|
||||
"load": "Load",
|
||||
"loading": "Loading…",
|
||||
"log_in": "Log In",
|
||||
"log_out": "Log out",
|
||||
"logged_in_as": "Logged in as",
|
||||
"login_enter_password_description": "Enter your password to access your JetKVM.",
|
||||
"login_enter_password": "Enter your password",
|
||||
"login_error": "An error occurred while logging in",
|
||||
"login_forgot_password": "Forgot password?",
|
||||
"login_password_label": "Password",
|
||||
"login_welcome_back": "Welcome back to JetKVM",
|
||||
"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_error": "An error occurred while saving.",
|
||||
"macro_save": "Save Macro",
|
||||
"macro_step_count": "{steps} / {max} 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…",
|
||||
"macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
|
||||
"macro_steps_label": "Steps",
|
||||
"metric_not_supported": "Metric not supported",
|
||||
"metric_waiting_for_data": "Waiting for data…",
|
||||
"mount_add_file_to_get_started": "Add a file to get started",
|
||||
"mount_add_new_media": "Add New Media",
|
||||
"mount_available_storage": "Available Storage",
|
||||
"mount_button_back_to_overview": "Back to Overview",
|
||||
"mount_button_cancel_upload": "Cancel Upload",
|
||||
"mount_button_continue_upload": "Continue uploading",
|
||||
"mount_button_mount_file": "Mount File",
|
||||
"mount_button_mount_url": "Mount URL",
|
||||
"mount_button_next": "Next",
|
||||
"mount_button_previous": "Previous",
|
||||
"mount_button_select": "Select",
|
||||
"mount_button_showing_results": "Showing {from} to {to} of {total} results",
|
||||
"mount_button_upload_new_image": "Upload a new image",
|
||||
"mount_bytes_free": "{bytesFree} free",
|
||||
"mount_bytes_used": "{bytesUsed} used",
|
||||
"mount_calculating": "Calculating…",
|
||||
"mount_click_to_select_file": "Click to select a file",
|
||||
"mount_click_to_select_incomplete": "Click to select \"{name}\"",
|
||||
"mount_confirm_delete": "Are you sure you want to delete {name}?",
|
||||
"mount_continue_uploading_with_name": "Continue uploading \"{name}\"",
|
||||
"mount_description_mode": "Choose how you want to mount your virtual media",
|
||||
"mount_error_delete_file": "Error deleting file: {error}",
|
||||
"mount_error_description": "An error occurred while attempting to mount the media. Please try again.",
|
||||
"mount_error_get_storage_space": "Error getting storage space: {error}",
|
||||
"mount_error_list_storage": "Error listing storage files: {error}",
|
||||
"mount_error_title": "Mount Error",
|
||||
"mount_get_state_error": "Failed to get virtual media state: {error}",
|
||||
"mount_jetkvm_storage_description": "Mount previously uploaded files from the JetKVM storage",
|
||||
"mount_jetkvm_storage": "JetKVM Storage Mount",
|
||||
"mount_label_mount_as": "Mount as",
|
||||
"mount_label_url_description": "Mount files from any public web address",
|
||||
"mount_label_url": "URL Mount",
|
||||
"mount_mode_cdrom": "CD/DVD",
|
||||
"mount_mode_disk": "Disk",
|
||||
"mount_mounted_as": "Mounted as",
|
||||
"mount_mounted_from_storage": "Mounted from JetKVM Storage",
|
||||
"mount_no_images_description": "Upload an image to start virtual media mounting.",
|
||||
"mount_no_images_title": "No images available",
|
||||
"mount_no_mounted_media": "No mounted media",
|
||||
"mount_percentage_used": "{percentageUsed}% used",
|
||||
"mount_please_select_file_to_upload": "Please select the file to upload.",
|
||||
"mount_please_select_file": "Please select the file \"{name}\" to continue the upload.",
|
||||
"mount_popular_images": "Popular images",
|
||||
"mount_streaming_from_url": "Streaming from URL",
|
||||
"mount_supported_formats": "Supported formats: ISO, IMG",
|
||||
"mount_tag_experimental": "Experimental",
|
||||
"mount_title_mode": "Virtual Media Source",
|
||||
"mount_unmount_error": "Failed to unmount image: {error}",
|
||||
"mount_unmount": "Unmount",
|
||||
"mount_upload_description": "Select an image file to upload to JetKVM storage",
|
||||
"mount_upload_error": "Upload error: {error}",
|
||||
"mount_upload_failed_datachannel": "Failed to create data channel for file upload",
|
||||
"mount_upload_failed_rtc": "Upload failed: {error}",
|
||||
"mount_upload_successful": "Upload successful",
|
||||
"mount_upload_title": "Upload New Image",
|
||||
"mount_uploaded_has_been_uploaded": "{name} has been uploaded",
|
||||
"mount_uploading_with_name": "Uploading {name}",
|
||||
"mount_uploading": "Uploading…",
|
||||
"mount_url_description": "Mount files from any public web address",
|
||||
"mount_url_input_label": "Image URL",
|
||||
"mount_url_mount": "URL Mount",
|
||||
"mount_view_device_description": "Select an image to mount from the JetKVM storage",
|
||||
"mount_view_device_title": "Mount from JetKVM Storage",
|
||||
"mount_view_url_description": "Enter an URL to the image file to mount",
|
||||
"mount_view_url_title": "Mount from URL",
|
||||
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
|
||||
"mount_virtual_media_source_description": "Choose how you want to mount your virtual media",
|
||||
"mount_virtual_media_source": "Virtual Media Source",
|
||||
"mount_virtual_media": "Virtual Media",
|
||||
"never_seen_online": "Never seen online",
|
||||
"next": "Next",
|
||||
"no_results_found": "No results found",
|
||||
"not_available": "N/A",
|
||||
"not_found": "Not found",
|
||||
"ntp_servers": "NTP Servers",
|
||||
"oh_no": "Oh no!",
|
||||
"online": "Online",
|
||||
"other_session_detected": "Another Active Session Detected",
|
||||
"other_session_take_over": " Only one active session is supported at a time. Would you like to take over this session?",
|
||||
"other_session_use_here_button": "Use Here",
|
||||
"page_not_found_description": "The page you were looking for does not exist.",
|
||||
"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_failed_paste": "Failed to paste text: {error}",
|
||||
"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}",
|
||||
"peer_connection_closed": "Closed",
|
||||
"peer_connection_closing": "Closing",
|
||||
"peer_connection_connected": "Connected",
|
||||
"peer_connection_connecting": "Connecting",
|
||||
"peer_connection_disconnected": "Disconnected",
|
||||
"peer_connection_error": "Connection error",
|
||||
"peer_connection_failed": "Connection failed",
|
||||
"peer_connection_new": "Connecting",
|
||||
"previous": "Previous",
|
||||
"register_device_error": "There was an error {error} registering your device.",
|
||||
"register_device_finish_button": "Finish Setup",
|
||||
"register_device_name_description": "Name your device so you can easily identify it later. You can change this name at any time.",
|
||||
"register_device_name_label": "Device Name",
|
||||
"register_device_name_placeholder": "Plex Media Server",
|
||||
"register_device_no_name": "Please specify a name",
|
||||
"rename_device_button": "Rename Device",
|
||||
"rename_device_description": "Properly name your device to easily identify it.",
|
||||
"rename_device_error": "There was an error {error} renaming your device.",
|
||||
"rename_device_headline": "Rename {name}",
|
||||
"rename_device_new_name_label": "New device name",
|
||||
"rename_device_new_name_placeholder": "Plex Media Server",
|
||||
"rename_device_no_name": "Please specify a name",
|
||||
"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_exit_fullscreen": "Exit Fullscreen",
|
||||
"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_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_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_voltage_unit": "V",
|
||||
"dc_power_control_voltage": "Voltage",
|
||||
"saving": "Saving…",
|
||||
"search_placeholder": "Search…",
|
||||
"serial_console_baud_rate": "Baud Rate",
|
||||
"serial_console_configure_description": "Configure your serial console settings",
|
||||
"serial_console_data_bits": "Data Bits",
|
||||
|
|
@ -112,191 +352,24 @@
|
|||
"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_save_device": "Save Device",
|
||||
"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_failed_paste": "Failed to paste text: {error}",
|
||||
"mount_add_file_to_get_started": "Add a file to get started",
|
||||
"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_mode_cdrom": "CD-ROM",
|
||||
"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_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_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_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_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",
|
||||
"serial_console": "Serial Console",
|
||||
"setting_remote_description": "Setting remote description",
|
||||
"setting_remote_session_description": "Setting remote session description...",
|
||||
"setting_up_connection_to_device": "Setting up connection to device...",
|
||||
"something_went_wrong": "Something went wrong. Please try again later or contact support",
|
||||
"step_counter_step": "Step {step}",
|
||||
"subnet_mask": "Subnet Mask",
|
||||
"troubleshoot_connection": "Troubleshoot Connection",
|
||||
"unknown_error": "Unknown error",
|
||||
"update_in_progress": "Update in Progress",
|
||||
"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",
|
||||
"updating_leave_device_on": "Please don't turn off your device…",
|
||||
"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_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",
|
||||
|
|
@ -314,10 +387,75 @@
|
|||
"usb_config_update_identifiers": "Update USB Identifiers",
|
||||
"usb_config_vendor_id_label": "Vendor ID",
|
||||
"usb_config_vendor_id_placeholder": "Enter Vendor ID",
|
||||
"usb_device_classes_description": "USB device classes in the composite device",
|
||||
"usb_device_classes_title": "Classes",
|
||||
"usb_device_custom": "Custom",
|
||||
"usb_device_description": "USB devices to emulate on the target computer",
|
||||
"usb_device_enable_absolute_mouse_description": "Enable Absolute Mouse (Pointer)",
|
||||
"usb_device_enable_absolute_mouse_title": "Enable Absolute Mouse (Pointer)",
|
||||
"usb_device_enable_keyboard_description": "Enable Keyboard",
|
||||
"usb_device_enable_keyboard_title": "Enable Keyboard",
|
||||
"usb_device_enable_mass_storage_description": "Sometimes it might need to be disabled to prevent issues with certain devices",
|
||||
"usb_device_enable_mass_storage_title": "Enable USB Mass Storage",
|
||||
"usb_device_enable_relative_mouse_description": "Enable Relative Mouse",
|
||||
"usb_device_enable_relative_mouse_title": "Enable Relative Mouse",
|
||||
"usb_device_failed_load": "Failed to load USB devices: {error}",
|
||||
"usb_device_failed_set": "Failed to set USB devices: {error}",
|
||||
"usb_device_keyboard_mouse_and_mass_storage": "Keyboard, Mouse and Mass Storage",
|
||||
"usb_device_keyboard_only": "Keyboard Only",
|
||||
"usb_device_restore_default": "Restore to Default",
|
||||
"usb_device_title": "USB Device",
|
||||
"usb_device_update_classes": "Update USB Classes",
|
||||
"usb_device_updated": "USB Devices updated",
|
||||
"usb_state_connected": "Connected",
|
||||
"usb_state_connecting": "Connecting",
|
||||
"usb_state_disconnected": "Disconnected",
|
||||
"usb_state_low_power_mode": "Low power mode",
|
||||
"usb": "USB",
|
||||
"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_disabled": "Pointer lock disabled",
|
||||
"video_pointer_lock_enabled": "Pointer lock enabled — press Escape to unlock",
|
||||
"view_details": "View Details",
|
||||
"virtual_keyboard_description": "Use the virtual keyboard to send special keys or key combinations to the remote computer.",
|
||||
"virtual_keyboard_header": "Virtual Keyboard",
|
||||
"virtual_keyboard_description": "Use the virtual keyboard to send special keys or key combinations to the remote computer."
|
||||
"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_save_device": "Save Device",
|
||||
"wake_on_lan_description": "Send a Magic Packet to wake up a remote device.",
|
||||
"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_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",
|
||||
"welcome_to_jetkvm_description": "Control any computer remotely",
|
||||
"welcome_to_jetkvm": "Welcome to JetKVM"
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "kvm-ui",
|
||||
"version": "2025.10.07.1700",
|
||||
"version": "2025.10.09.0200",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kvm-ui",
|
||||
"version": "2025.10.07.1700",
|
||||
"version": "2025.10.09.0200",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
|
|
@ -30,8 +30,8 @@
|
|||
"react-dom": "^19.2.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.3",
|
||||
"react-simple-keyboard": "^3.8.126",
|
||||
"react-router": "^7.9.4",
|
||||
"react-simple-keyboard": "^3.8.127",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.2.1",
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^6.1.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.23",
|
||||
"globals": "^16.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
|
|
@ -3065,9 +3065,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.13",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz",
|
||||
"integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==",
|
||||
"version": "2.8.14",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.14.tgz",
|
||||
"integrity": "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
@ -3188,9 +3188,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001748",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz",
|
||||
"integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==",
|
||||
"version": "1.0.30001749",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
|
||||
"integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -3671,9 +3671,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.232",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz",
|
||||
"integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==",
|
||||
"version": "1.5.233",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz",
|
||||
"integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
|
@ -3862,9 +3862,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.10",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
||||
"integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz",
|
||||
"integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
|
|
@ -4166,14 +4166,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz",
|
||||
"integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.0.tgz",
|
||||
"integrity": "sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"hermes-parser": "^0.25.1",
|
||||
"zod": "^3.22.4 || ^4.0.0",
|
||||
"zod-validation-error": "^3.0.3 || ^4.0.0"
|
||||
},
|
||||
|
|
@ -4860,6 +4861,23 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hermes-estree": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||
"integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hermes-parser": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
|
||||
"integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/human-id": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.2.tgz",
|
||||
|
|
@ -6458,9 +6476,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
|
||||
"integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
|
||||
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
|
|
@ -6480,9 +6498,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-simple-keyboard": {
|
||||
"version": "3.8.126",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.126.tgz",
|
||||
"integrity": "sha512-eULRTm9Rrvo72+WnU9h3fTooSLMgJ52Yi5VvVm0SirMKQnigRgOlcx1VJeVdFVN4JfJEuSN1voV+Vsmeheg3vA==",
|
||||
"version": "3.8.127",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.127.tgz",
|
||||
"integrity": "sha512-CncdXLnJ3tBlB6iEHtkgj5W21ns/DdKKO1bCy9pLWey5xONf+KAComVVnDsnAaC0b4LLI7frWBDjOT01vj8dew==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
|
|
@ -6758,9 +6776,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "kvm-ui",
|
||||
"private": true,
|
||||
"version": "2025.10.07.1700",
|
||||
"version": "2025.10.09.0200",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^22.15.0"
|
||||
|
|
@ -43,8 +43,8 @@
|
|||
"react-dom": "^19.2.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.3",
|
||||
"react-simple-keyboard": "^3.8.126",
|
||||
"react-router": "^7.9.4",
|
||||
"react-simple-keyboard": "^3.8.127",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.2.1",
|
||||
|
|
@ -75,10 +75,10 @@
|
|||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^6.1.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.23",
|
||||
"globals": "^16.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ 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 { cx } from "@/cva.config";
|
||||
import {
|
||||
useHidStore,
|
||||
useMountMediaStore,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { JSX } from "react";
|
||||
import { Link, type FetcherWithComponents, type LinkProps, useNavigation } from "react-router";
|
||||
import { cva, cx } from "@/cva.config";
|
||||
|
||||
import { cva, cx } from "@/cva.config";
|
||||
import ExtLink from "@components/ExtLink";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ 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 { useHidStore, useRTCStore, useUserStore } from "@hooks/stores";
|
||||
import Card from "@components/Card";
|
||||
import Container from "@components/Container";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import {
|
||||
useHidStore,
|
||||
|
|
@ -26,17 +26,8 @@ export default function InfoBar() {
|
|||
(state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`,
|
||||
);
|
||||
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { debugMode, mouseMode, showPressedKeys } = useSettingsStore();
|
||||
const { isPasteInProgress } = useHidStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpcDataChannel) return;
|
||||
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
|
||||
rpcDataChannel.onerror = (e: Event) =>
|
||||
console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||
}, [rpcDataChannel]);
|
||||
|
||||
const { keyboardLedState, usbState } = useHidStore();
|
||||
const { isTurnServerInUse } = useRTCStore();
|
||||
const { hdmiState } = useVideoStore();
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ 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 { cx } from "@/cva.config";
|
||||
import { AvailableTerminalTypes, useUiStore } from "@hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
|
@ -54,6 +54,7 @@ const TERMINAL_CONFIG = {
|
|||
// Add these configurations:
|
||||
cursorStyle: "block",
|
||||
rendererType: "canvas", // Ensure we're using the canvas renderer
|
||||
unicode: { activeVersion: "11" }
|
||||
} as const;
|
||||
|
||||
function Terminal({
|
||||
|
|
@ -144,7 +145,6 @@ function Terminal({
|
|||
instance.loadAddon(new ClipboardAddon());
|
||||
instance.loadAddon(new Unicode11Addon());
|
||||
instance.loadAddon(new WebLinksAddon());
|
||||
instance.unicode.activeVersion = "11";
|
||||
|
||||
if (isWebGl2Supported) {
|
||||
const webGl2Addon = new WebglAddon();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from "react";
|
|||
|
||||
import { cx } from "@/cva.config";
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { cx } from "@/cva.config";
|
||||
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Keyboard from "react-simple-keyboard";
|
||||
import { LuKeyboard } from "react-icons/lu";
|
||||
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import useKeyboard from "@hooks/useKeyboard";
|
||||
import useMouse from "@hooks/useMouse";
|
||||
import {
|
||||
|
|
@ -234,6 +234,18 @@ export default function WebRTCVideo() {
|
|||
[getMouseWheelHandler],
|
||||
);
|
||||
|
||||
function getAdjustedKeyCode(e: KeyboardEvent) {
|
||||
const key = e.key;
|
||||
let code = e.code;
|
||||
|
||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||
code = "Backquote";
|
||||
} else if (code == "Backquote" && ["§", "±"].includes(key)) {
|
||||
code = "IntlBackslash";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
const keyDownHandler = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -468,17 +480,6 @@ export default function WebRTCVideo() {
|
|||
};
|
||||
}, [videoSaturation, videoBrightness, videoContrast]);
|
||||
|
||||
function getAdjustedKeyCode(e: KeyboardEvent) {
|
||||
const key = e.key;
|
||||
let code = e.code;
|
||||
|
||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||
code = "Backquote";
|
||||
} else if (code == "Backquote" && ["§", "±"].includes(key)) {
|
||||
code = "IntlBackslash";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
import { useRTCStore } from "@hooks/stores";
|
||||
|
||||
import {
|
||||
CancelKeyboardMacroReportMessage,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
import { useRTCStore } from "@hooks/stores";
|
||||
|
||||
export interface JsonRpcRequest {
|
||||
jsonrpc: string;
|
||||
|
|
|
|||
|
|
@ -149,66 +149,6 @@ export default function useKeyboard() {
|
|||
}
|
||||
}, [rpcHidReady, sendKeyboardEventHidRpc, handleLegacyKeyboardReport, cancelKeepAlive]);
|
||||
|
||||
// handleKeyPress is used to handle a key press or release event.
|
||||
// This function handle both key press and key release events.
|
||||
// It checks if the keyPressReport API is available and sends the key press event.
|
||||
// If the keyPressReport API is not available, it simulates the device-side key
|
||||
// handling for legacy devices and updates the keysDownState accordingly.
|
||||
// It then sends the full keyboard state to the device.
|
||||
|
||||
const sendKeypress = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
cancelKeepAlive();
|
||||
|
||||
sendKeypressEventHidRpc(key, press);
|
||||
|
||||
if (press) {
|
||||
scheduleKeepAlive();
|
||||
}
|
||||
},
|
||||
[sendKeypressEventHidRpc, scheduleKeepAlive, cancelKeepAlive],
|
||||
);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
async (key: number, press: boolean) => {
|
||||
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
|
||||
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
|
||||
|
||||
if (rpcHidReady) {
|
||||
// if the keyPress api is available, we can just send the key press event
|
||||
// sendKeypressEvent is used to send a single key press/release event to the device.
|
||||
// It sends the key and whether it is pressed or released.
|
||||
// Older device version doesn't support this API, so we will switch to local key handling
|
||||
// In that case we will switch to local key handling and update the keysDownState
|
||||
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||
sendKeypress(key, press);
|
||||
} else {
|
||||
// Older backends don't support the hidRpc API, so we need:
|
||||
// 1. Calculate the state
|
||||
// 2. Send the newly calculated state to the device
|
||||
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
keysDownState,
|
||||
key,
|
||||
press,
|
||||
);
|
||||
|
||||
handleLegacyKeyboardReport(downState.keys, downState.modifier);
|
||||
|
||||
// if we just sent ErrorRollOver, reset to empty state
|
||||
if (downState.keys[0] === hidErrorRollOver) {
|
||||
resetKeyboardState();
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
rpcDataChannel?.readyState,
|
||||
rpcHidReady,
|
||||
keysDownState,
|
||||
handleLegacyKeyboardReport,
|
||||
resetKeyboardState,
|
||||
sendKeypress,
|
||||
],
|
||||
);
|
||||
|
||||
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
||||
function simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
|
|
@ -273,6 +213,66 @@ export default function useKeyboard() {
|
|||
return { modifier: modifiers, keys };
|
||||
}
|
||||
|
||||
const sendKeypress = useCallback(
|
||||
(key: number, press: boolean) => {
|
||||
cancelKeepAlive();
|
||||
|
||||
sendKeypressEventHidRpc(key, press);
|
||||
|
||||
if (press) {
|
||||
scheduleKeepAlive();
|
||||
}
|
||||
},
|
||||
[sendKeypressEventHidRpc, scheduleKeepAlive, cancelKeepAlive],
|
||||
);
|
||||
|
||||
// handleKeyPress is used to handle a key press or release event.
|
||||
// This function handle both key press and key release events.
|
||||
// It checks if the keyPressReport API is available and sends the key press event.
|
||||
// If the keyPressReport API is not available, it simulates the device-side key
|
||||
// handling for legacy devices and updates the keysDownState accordingly.
|
||||
// It then sends the full keyboard state to the device.
|
||||
const handleKeyPress = useCallback(
|
||||
async (key: number, press: boolean) => {
|
||||
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
|
||||
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
|
||||
|
||||
if (rpcHidReady) {
|
||||
// if the keyPress api is available, we can just send the key press event
|
||||
// sendKeypressEvent is used to send a single key press/release event to the device.
|
||||
// It sends the key and whether it is pressed or released.
|
||||
// Older device version doesn't support this API, so we will switch to local key handling
|
||||
// In that case we will switch to local key handling and update the keysDownState
|
||||
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||
sendKeypress(key, press);
|
||||
} else {
|
||||
// Older backends don't support the hidRpc API, so we need:
|
||||
// 1. Calculate the state
|
||||
// 2. Send the newly calculated state to the device
|
||||
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
|
||||
keysDownState,
|
||||
key,
|
||||
press,
|
||||
);
|
||||
|
||||
handleLegacyKeyboardReport(downState.keys, downState.modifier);
|
||||
|
||||
// if we just sent ErrorRollOver, reset to empty state
|
||||
if (downState.keys[0] === hidErrorRollOver) {
|
||||
resetKeyboardState();
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
rpcDataChannel?.readyState,
|
||||
rpcHidReady,
|
||||
keysDownState,
|
||||
handleLegacyKeyboardReport,
|
||||
resetKeyboardState,
|
||||
sendKeypress,
|
||||
],
|
||||
);
|
||||
|
||||
// Cleanup function to cancel keepalive timer
|
||||
const cleanup = useCallback(() => {
|
||||
cancelKeepAlive();
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ if (isOnDevice) {
|
|||
path: "/",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <DeviceRoute />,
|
||||
HydrateFallback: () => <div className="p-4">Loading...</div>,
|
||||
HydrateFallback: () => <div className="p-4">{m.loading()}</div>,
|
||||
loader: DeviceRoute.loader,
|
||||
children: [
|
||||
{
|
||||
|
|
@ -391,14 +391,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const errorMessage = error?.data?.error?.message || error?.message;
|
||||
if (isRouteErrorResponse(error)) {
|
||||
if (error.status === 404) return <NotFoundPage />;
|
||||
}
|
||||
|
||||
const errorMessage: string | null = error?.data?.error?.message ?? error?.message ?? null;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
|
||||
import React, { useEffect } from "react";
|
||||
import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
|
||||
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import Card from "@components/Card";
|
||||
|
||||
interface NotificationOptions {
|
||||
duration?: number;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
|||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||
|
||||
import { User } from "@hooks/stores";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { CardHeader } from "@components/CardHeader";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import { User } from "@/hooks/stores";
|
||||
import { checkAuth } from "@/main";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { checkAuth } from "@/main";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
interface LoaderData {
|
||||
device: { id: string; name: string; user: { googleId: string } };
|
||||
|
|
@ -28,11 +29,12 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
|||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return { message: "There was an error deregistering your device. Please try again." };
|
||||
return { message: m.deregister_error({ status: res.statusText }) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { message: "There was an error deregistering your device. Please try again." };
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
return { message: m.deregister_error({ status: message }) };
|
||||
}
|
||||
|
||||
return redirect("/devices");
|
||||
|
|
@ -68,7 +70,7 @@ export default function DevicesIdDeregister() {
|
|||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<DashboardNavbar
|
||||
isLoggedIn={!!user}
|
||||
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
|
||||
primaryLinks={[{ title: m.deregister_cloud_devices(), to: "/devices" }]}
|
||||
userEmail={user?.email}
|
||||
picture={user?.picture}
|
||||
kvmName={device?.name}
|
||||
|
|
@ -82,21 +84,14 @@ export default function DevicesIdDeregister() {
|
|||
size="SM"
|
||||
theme="blank"
|
||||
LeadingIcon={ChevronLeftIcon}
|
||||
text="Back to Devices"
|
||||
text={m.back_to_devices()}
|
||||
to="/devices"
|
||||
/>
|
||||
<Card className="max-w-3xl p-6">
|
||||
<div className="max-w-xl space-y-4">
|
||||
<CardHeader
|
||||
headline={`Deregister ${device.name || device.id} from your cloud account`}
|
||||
description={
|
||||
<>
|
||||
This will remove the device from your cloud account and revoke
|
||||
remote access to it.
|
||||
<br />
|
||||
Please note that local access will still be possible
|
||||
</>
|
||||
}
|
||||
headline={m.deregister_headline({ device: device.name || device.id })}
|
||||
description={m.deregister_description()}
|
||||
/>
|
||||
|
||||
<Fieldset>
|
||||
|
|
@ -107,20 +102,20 @@ export default function DevicesIdDeregister() {
|
|||
size="MD"
|
||||
theme="light"
|
||||
to="/devices"
|
||||
text="Cancel"
|
||||
text={m.cancel()}
|
||||
textAlign="center"
|
||||
/>
|
||||
<Button
|
||||
size="MD"
|
||||
theme="danger"
|
||||
type="submit"
|
||||
text="Deregister from Cloud"
|
||||
text={m.deregister_button()}
|
||||
textAlign="center"
|
||||
/>
|
||||
</div>
|
||||
{error?.message && (
|
||||
<p className="text-sm text-red-500 dark:text-red-400">
|
||||
{error?.message}
|
||||
{m.deregister_error({ status: error.message })}
|
||||
</p>
|
||||
)}
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
LuLink,
|
||||
LuRadioReceiver,
|
||||
|
|
@ -7,28 +8,28 @@ import {
|
|||
} from "react-icons/lu";
|
||||
import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||
import { useNavigate } from "react-router";
|
||||
import DebianIcon from "@assets/debian-icon.png";
|
||||
import UbuntuIcon from "@assets/ubuntu-icon.png";
|
||||
import FedoraIcon from "@assets/fedora-icon.png";
|
||||
import OpenSUSEIcon from "@assets/opensuse-icon.png";
|
||||
import ArchIcon from "@assets/arch-icon.png";
|
||||
import NetBootIcon from "@assets/netboot-icon.svg";
|
||||
import LogoBlueIcon from "@assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
import Card, { GridCard } from "@/components/Card";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import { formatters } from "@/utils";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import AutoHeight from "@components/AutoHeight";
|
||||
import { Button } from "@components/Button";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import DebianIcon from "@/assets/debian-icon.png";
|
||||
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
||||
import FedoraIcon from "@/assets/fedora-icon.png";
|
||||
import OpenSUSEIcon from "@/assets/opensuse-icon.png";
|
||||
import ArchIcon from "@/assets/arch-icon.png";
|
||||
import NetBootIcon from "@/assets/netboot-icon.svg";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { formatters } from "@/utils";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import { isOnDevice } from "@/main";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import notifications from "../notifications";
|
||||
import { isOnDevice } from "../main";
|
||||
import { cx } from "../cva.config";
|
||||
import {
|
||||
MountMediaState,
|
||||
RemoteVirtualMediaState,
|
||||
|
|
@ -145,12 +146,12 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<img
|
||||
src={LogoBlueIcon}
|
||||
alt="JetKVM Logo"
|
||||
alt={m.jetkvm_logo()}
|
||||
className="block h-[24px] dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src={LogoWhiteIcon}
|
||||
alt="JetKVM Logo"
|
||||
alt={m.jetkvm_logo()}
|
||||
className="hidden h-[24px] dark:mt-0! dark:block"
|
||||
/>
|
||||
{modalView === "mode" && (
|
||||
|
|
@ -238,26 +239,26 @@ function ModeSelectionView({
|
|||
<div className="w-full space-y-4">
|
||||
<div className="animate-fadeIn space-y-0 opacity-0">
|
||||
<h2 className="text-lg leading-tight font-bold dark:text-white">
|
||||
Virtual Media Source
|
||||
{m.mount_virtual_media_source()}
|
||||
</h2>
|
||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
Choose how you want to mount your virtual media
|
||||
{m.mount_virtual_media_source_description()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
label: "URL Mount",
|
||||
label: m.mount_url_mount(),
|
||||
value: "url",
|
||||
description: "Mount files from any public web address",
|
||||
description: m.mount_url_description(),
|
||||
icon: LuLink,
|
||||
tag: "Experimental",
|
||||
tag: m.experimental(),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "JetKVM Storage Mount",
|
||||
label: m.mount_jetkvm_storage(),
|
||||
value: "device",
|
||||
description: "Mount previously uploaded files from the JetKVM storage",
|
||||
description: m.mount_jetkvm_storage_description(),
|
||||
icon: LuRadioReceiver,
|
||||
tag: null,
|
||||
disabled: false,
|
||||
|
|
@ -332,7 +333,7 @@ function ModeSelectionView({
|
|||
onClick={() => {
|
||||
setModalView(selectedMode);
|
||||
}}
|
||||
text="Continue"
|
||||
text={m.continue()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -410,8 +411,8 @@ function UrlView({
|
|||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from URL"
|
||||
description="Enter an URL to the image file to mount"
|
||||
title={m.mount_view_url_title()}
|
||||
description={m.mount_view_url_description()}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
@ -423,7 +424,7 @@ function UrlView({
|
|||
<InputFieldWithLabel
|
||||
placeholder="https://example.com/image.iso"
|
||||
type="url"
|
||||
label="Image URL"
|
||||
label={m.mount_url_input_label()}
|
||||
ref={urlRef}
|
||||
value={url}
|
||||
onChange={e => handleUrlChange(e.target.value)}
|
||||
|
|
@ -440,12 +441,12 @@ function UrlView({
|
|||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
||||
</Fieldset>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={onBack} />
|
||||
<Button size="MD" theme="blank" text={m.back()} onClick={onBack} />
|
||||
<Button
|
||||
size="MD"
|
||||
theme="primary"
|
||||
loading={mountInProgress}
|
||||
text="Mount URL"
|
||||
text={m.mount_button_mount_url()}
|
||||
onClick={() => onMount(url, usbMode)}
|
||||
disabled={
|
||||
mountInProgress || !urlRef.current?.validity.valid || url.length === 0
|
||||
|
|
@ -463,7 +464,7 @@ function UrlView({
|
|||
}}
|
||||
>
|
||||
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
|
||||
Popular images
|
||||
{m.mount_popular_images()}
|
||||
</h2>
|
||||
<Card className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||
{popularImages.map((image, index) => (
|
||||
|
|
@ -487,7 +488,7 @@ function UrlView({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Select"
|
||||
text={m.mount_button_select()}
|
||||
onClick={() => handleUrlChange(image.url)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -553,7 +554,7 @@ function DeviceFileView({
|
|||
const syncStorage = useCallback(() => {
|
||||
send("listStorageFiles", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Error listing storage files: ${resp.error}`);
|
||||
notifications.error(m.mount_error_list_storage({ error: resp.error }));
|
||||
return;
|
||||
}
|
||||
const { files } = resp.result as StorageFiles;
|
||||
|
|
@ -568,7 +569,7 @@ function DeviceFileView({
|
|||
|
||||
send("getStorageSpace", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Error getting storage space: ${resp.error}`);
|
||||
notifications.error(m.mount_error_get_storage_space({ error: resp.error }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -597,7 +598,7 @@ function DeviceFileView({
|
|||
console.log("Deleting file:", file);
|
||||
send("deleteStorageFile", { filename: file.name }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Error deleting file: ${resp.error}`);
|
||||
notifications.error(m.mount_error_delete_file({ error: resp.error }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -630,8 +631,8 @@ function DeviceFileView({
|
|||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from JetKVM Storage"
|
||||
description="Select an image to mount from the JetKVM storage"
|
||||
title={m.mount_view_device_title()}
|
||||
description={m.mount_view_device_description()}
|
||||
/>
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
|
|
@ -647,17 +648,17 @@ function DeviceFileView({
|
|||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
No images available
|
||||
{m.mount_no_images_title()}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Upload an image to start virtual media mounting.
|
||||
{m.mount_no_images_description()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Upload a new image"
|
||||
text={m.mount_upload_title()}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -677,9 +678,7 @@ function DeviceFileView({
|
|||
const selectedFile = onStorageFiles.find(f => f.name === file.name);
|
||||
if (!selectedFile) return;
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete " + selectedFile.name + "?",
|
||||
)
|
||||
window.confirm(m.mount_confirm_delete({ name: selectedFile.name }))
|
||||
) {
|
||||
handleDeleteFile(selectedFile);
|
||||
}
|
||||
|
|
@ -692,24 +691,24 @@ function DeviceFileView({
|
|||
{onStorageFiles.length > filesPerPage && (
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||
Showing <span className="font-bold">{indexOfFirstFile + 1}</span> to{" "}
|
||||
<span className="font-bold">
|
||||
{Math.min(indexOfLastFile, onStorageFiles.length)}
|
||||
</span>{" "}
|
||||
of <span className="font-bold">{onStorageFiles.length}</span> results
|
||||
{m.mount_button_showing_results({
|
||||
from: indexOfFirstFile + 1,
|
||||
to: Math.min(indexOfLastFile, onStorageFiles.length),
|
||||
total: onStorageFiles.length
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Previous"
|
||||
text={m.previous()}
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Next"
|
||||
text={m.next()}
|
||||
onClick={handleNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
/>
|
||||
|
|
@ -738,7 +737,7 @@ function DeviceFileView({
|
|||
size="MD"
|
||||
disabled={selected === null || mountInProgress}
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
text={m.mount_button_mount_file()}
|
||||
loading={mountInProgress}
|
||||
onClick={() =>
|
||||
onMountStorageFile(
|
||||
|
|
@ -772,10 +771,10 @@ function DeviceFileView({
|
|||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Available Storage
|
||||
{m.mount_available_storage()}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{percentageUsed}% used
|
||||
{m.mount_percentage_used({ percentageUsed })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700">
|
||||
|
|
@ -786,10 +785,10 @@ function DeviceFileView({
|
|||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesUsed)} used
|
||||
{m.mount_bytes_used({ bytesUsed: formatters.bytes(bytesUsed) })}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesFree)} free
|
||||
{m.mount_bytes_free({ bytesFree: formatters.bytes(bytesFree) })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -806,7 +805,7 @@ function DeviceFileView({
|
|||
size="MD"
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new image"
|
||||
text={m.mount_button_upload_new_image()}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -862,7 +861,7 @@ function UploadFileView({
|
|||
|
||||
if (!rtcDataChannel) {
|
||||
console.error("Failed to create data channel for file upload");
|
||||
notifications.error("Failed to create data channel for file upload");
|
||||
notifications.error(m.mount_upload_failed_datachannel());
|
||||
setUploadState("idle");
|
||||
console.log("Upload state set to 'idle'");
|
||||
|
||||
|
|
@ -952,7 +951,7 @@ function UploadFileView({
|
|||
|
||||
rtcDataChannel.onerror = error => {
|
||||
console.error("RTC Data channel error:", error);
|
||||
notifications.error(`Upload failed: ${error}`);
|
||||
notifications.error(m.mount_upload_failed_rtc({ error: error }));
|
||||
setUploadState("idle");
|
||||
console.log("Upload state set to 'idle'");
|
||||
};
|
||||
|
|
@ -1037,7 +1036,7 @@ function UploadFileView({
|
|||
file.name !== incompleteFileName.replace(".incomplete", "")
|
||||
) {
|
||||
setFileError(
|
||||
`Please select the file "${incompleteFileName.replace(".incomplete", "")}" to continue the upload.`,
|
||||
m.mount_please_select_file({ name: incompleteFileName.replace(".incomplete", "") }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1080,11 +1079,11 @@ function UploadFileView({
|
|||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Upload New Image"
|
||||
title={m.mount_upload_title()}
|
||||
description={
|
||||
incompleteFileName
|
||||
? `Continue uploading "${incompleteFileName}"`
|
||||
: "Select an image file to upload to JetKVM storage"
|
||||
? m.mount_continue_uploading_with_name({ name: incompleteFileName.replace(".incomplete", "") })
|
||||
: m.mount_upload_description()
|
||||
}
|
||||
/>
|
||||
<div
|
||||
|
|
@ -1121,11 +1120,11 @@ function UploadFileView({
|
|||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
{incompleteFileName
|
||||
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
|
||||
: "Click to select a file"}
|
||||
? m.mount_click_to_select_incomplete({ name: incompleteFileName.replace(".incomplete", "") })
|
||||
: m.mount_click_to_select_file()}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Supported formats: ISO, IMG
|
||||
{m.mount_supported_formats()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1140,7 +1139,7 @@ function UploadFileView({
|
|||
</Card>
|
||||
</div>
|
||||
<h3 className="leading-non text-lg font-semibold text-black dark:text-white">
|
||||
Uploading {formatters.truncateMiddle(uploadedFileName, 30)}
|
||||
{m.mount_uploading_with_name({ name: formatters.truncateMiddle(uploadedFileName, 30) })}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(uploadedFileSize || 0)}
|
||||
|
|
@ -1153,11 +1152,11 @@ function UploadFileView({
|
|||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-slate-600 dark:text-slate-400">
|
||||
<span>Uploading...</span>
|
||||
<span>{m.mount_uploading()}</span>
|
||||
<span>
|
||||
{uploadSpeed !== null
|
||||
? `${formatters.bytes(uploadSpeed)}/s`
|
||||
: "Calculating..."}
|
||||
: m.mount_calculating()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1174,11 +1173,10 @@ function UploadFileView({
|
|||
</Card>
|
||||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
Upload successful
|
||||
{m.mount_upload_successful()}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.truncateMiddle(uploadedFileName, 40)} has been
|
||||
uploaded
|
||||
{m.mount_uploaded_has_been_uploaded({ name: formatters.truncateMiddle(uploadedFileName, 40) })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1205,7 +1203,7 @@ function UploadFileView({
|
|||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400 opacity-0"
|
||||
style={{ animationDuration: "0.7s" }}
|
||||
>
|
||||
Error: {uploadError}
|
||||
{m.mount_upload_error({ error: String(uploadError) })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1221,7 +1219,7 @@ function UploadFileView({
|
|||
<Button
|
||||
size="MD"
|
||||
theme="light"
|
||||
text="Cancel Upload"
|
||||
text={m.mount_button_cancel_upload()}
|
||||
onClick={() => {
|
||||
onCancelUpload();
|
||||
setUploadState("idle");
|
||||
|
|
@ -1235,7 +1233,7 @@ function UploadFileView({
|
|||
<Button
|
||||
size="MD"
|
||||
theme={uploadState === "success" ? "primary" : "light"}
|
||||
text="Back to Overview"
|
||||
text={m.mount_button_back_to_overview()}
|
||||
onClick={onBack}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1259,10 +1257,10 @@ function ErrorView({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-red-600">
|
||||
<ExclamationTriangleIcon className="h-6 w-6" />
|
||||
<h2 className="text-lg leading-tight font-bold">Mount Error</h2>
|
||||
<h2 className="text-lg leading-tight font-bold">{m.mount_error_title()}</h2>
|
||||
</div>
|
||||
<p className="text-sm leading-snug text-slate-600">
|
||||
An error occurred while attempting to mount the media. Please try again.
|
||||
{m.mount_error_description()}
|
||||
</p>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
|
|
@ -1271,8 +1269,8 @@ function ErrorView({
|
|||
</Card>
|
||||
)}
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button size="SM" theme="light" text="Close" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text="Back to Overview" onClick={onRetry} />
|
||||
<Button size="SM" theme="light" text={m.close()} onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.mount_button_back_to_overview()} onClick={onRetry} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1341,7 +1339,7 @@ function PreUploadedImageItem({
|
|||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={TrashIcon}
|
||||
text="Delete"
|
||||
text={m.delete()}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
|
|
@ -1362,7 +1360,7 @@ function PreUploadedImageItem({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Continue uploading"
|
||||
text={m.mount_button_continue_upload()}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onContinueUpload();
|
||||
|
|
@ -1408,7 +1406,7 @@ function UsbModeSelector({
|
|||
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
CD/DVD
|
||||
{m.mount_mode_cdrom()}
|
||||
</span>
|
||||
</label>
|
||||
<label htmlFor="disk" className="flex items-center">
|
||||
|
|
@ -1421,7 +1419,7 @@ function UsbModeSelector({
|
|||
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
Disk
|
||||
{m.mount_mode_disk()}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useNavigate, useOutletContext } from "react-router";
|
||||
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlue from "@/assets/logo-blue.svg";
|
||||
import LogoWhite from "@/assets/logo-white.svg";
|
||||
import { GridCard } from "@components/Card";
|
||||
import LogoBlue from "@assets/logo-blue.svg";
|
||||
import LogoWhite from "@assets/logo-white.svg";
|
||||
import { m } from "@localizations/messages";
|
||||
|
||||
interface ContextType {
|
||||
setupPeerConnection: () => Promise<void>;
|
||||
|
|
@ -30,14 +31,13 @@ export default function OtherSessionRoute() {
|
|||
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold dark:text-white">
|
||||
Another Active Session Detected
|
||||
{m.other_session_detected()}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
Only one active session is supported at a time. Would you like to take over
|
||||
this session?
|
||||
{m.other_session_take_over()}
|
||||
</p>
|
||||
<div className="flex items-center justify-start space-x-4">
|
||||
<Button size="SM" theme="primary" text="Use Here" onClick={handleClose} />
|
||||
<Button size="SM" theme="primary" text={m.other_session_use_here_button()} onClick={handleClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
|||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||
|
||||
import { User } from "@hooks/stores";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { CardHeader } from "@components/CardHeader";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import { User } from "@/hooks/stores";
|
||||
import { checkAuth } from "@/main";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { checkAuth } from "@/main";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import api from "@/api";
|
||||
import { m } from "@localizations/messages";
|
||||
|
||||
interface LoaderData {
|
||||
device: { id: string; name: string; user: { googleId: string } };
|
||||
|
|
@ -24,7 +24,7 @@ const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) =
|
|||
const { name } = Object.fromEntries(await request.formData());
|
||||
|
||||
if (!name || name === "") {
|
||||
return { message: "Please specify a name" };
|
||||
return { message: m.rename_device_no_name() };
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -32,11 +32,11 @@ const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) =
|
|||
name,
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { message: "There was an error renaming your device. Please try again." };
|
||||
return { message: m.rename_device_error({ error: res.statusText }) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { message: "There was an error renaming your device. Please try again." };
|
||||
return { message: m.rename_device_error({ error: String(e) }) };
|
||||
}
|
||||
|
||||
return redirect("/devices");
|
||||
|
|
@ -86,24 +86,24 @@ export default function DeviceIdRename() {
|
|||
size="SM"
|
||||
theme="blank"
|
||||
LeadingIcon={ChevronLeftIcon}
|
||||
text="Back to Devices"
|
||||
text={m.back_to_devices()}
|
||||
to="/devices"
|
||||
/>
|
||||
<Card className="max-w-3xl p-6">
|
||||
<div className="space-y-4">
|
||||
<CardHeader
|
||||
headline={`Rename ${device.name || device.id}`}
|
||||
description="Properly name your device to easily identify it."
|
||||
headline={m.rename_device_headline({ name: device.name || device.id })}
|
||||
description={m.rename_device_description()}
|
||||
/>
|
||||
|
||||
<Fieldset>
|
||||
<Form method="POST" className="max-w-sm space-y-4">
|
||||
<div className="group relative">
|
||||
<InputFieldWithLabel
|
||||
label="New device name"
|
||||
label={m.rename_device_new_name_label()}
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Plex Media Server"
|
||||
placeholder={m.rename_device_new_name_placeholder()}
|
||||
size="MD"
|
||||
autoFocus
|
||||
error={error?.message.toString()}
|
||||
|
|
@ -114,7 +114,7 @@ export default function DeviceIdRename() {
|
|||
size="MD"
|
||||
theme="primary"
|
||||
type="submit"
|
||||
text="Rename Device"
|
||||
text={m.rename_device_button()}
|
||||
textAlign="center"
|
||||
/>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import Fieldset from "@components/Fieldset";
|
|||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { Button } from "@components/Button";
|
||||
import { checkAuth } from "@/main";
|
||||
import api from "@/api";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||
await checkAuth();
|
||||
|
|
@ -31,12 +31,22 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
|||
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||
// Handle form submission
|
||||
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
||||
|
||||
if (!name || name === "") {
|
||||
return { message: m.register_device_no_name() };
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
|
||||
|
||||
if (res.ok) {
|
||||
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
||||
} else {
|
||||
return { error: "There was an error registering your device" };
|
||||
return { error: m.register_device_error({ error:res.statusText }) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { message: m.register_device_error({ error: String(e) }) };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -61,21 +71,19 @@ export default function SetupRoute() {
|
|||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">Let's name your device</h1>
|
||||
<p className="text-slate-600 dark:text-slate-400">
|
||||
Name your device so you can easily identify it later. You can change
|
||||
this name at any time.
|
||||
{m.register_device_name_description()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Fieldset className="space-y-12">
|
||||
<Form method="POST" className="max-w-sm mx-auto space-y-4">
|
||||
<InputFieldWithLabel
|
||||
label="Device Name"
|
||||
label={m.register_device_name_label()}
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Plex Media Server"
|
||||
placeholder={m.register_device_name_placeholder()}
|
||||
autoFocus
|
||||
data-1p-ignore
|
||||
autoComplete="organization"
|
||||
error={action?.error?.toString()}
|
||||
/>
|
||||
|
||||
|
|
@ -86,7 +94,7 @@ export default function SetupRoute() {
|
|||
theme="primary"
|
||||
fullWidth
|
||||
type="submit"
|
||||
text="Finish Setup"
|
||||
text={m.register_device_finish_button()}
|
||||
textAlign="center"
|
||||
/>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ import { FocusTrap } from "focus-trap-react";
|
|||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import api from "@/api";
|
||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
KeyboardLedState,
|
||||
KeysDownState,
|
||||
|
|
@ -33,23 +33,24 @@ import {
|
|||
useUpdateStore,
|
||||
useVideoStore,
|
||||
VideoState,
|
||||
} from "@/hooks/stores";
|
||||
} from "@hooks/stores";
|
||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { useVersion } from "@hooks/useVersion";
|
||||
import WebRTCVideo from "@components/WebRTCVideo";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats'));
|
||||
const ConnectionStatsSidebar = lazy(() => import('@components/sidebar/connectionStats'));
|
||||
const Terminal = lazy(() => import('@components/Terminal'));
|
||||
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||
import Modal from "@/components/Modal";
|
||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
const UpdateInProgressStatusCard = lazy(() => import("@components/UpdateInProgressStatusCard"));
|
||||
import Modal from "@components/Modal";
|
||||
import {
|
||||
ConnectionFailedOverlay,
|
||||
LoadingConnectionOverlay,
|
||||
PeerConnectionDisconnectedOverlay,
|
||||
} from "@/components/VideoOverlay";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||
} from "@components/VideoOverlay";
|
||||
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
||||
import { DeviceStatus } from "@routes/welcome-local";
|
||||
import { useVersion } from "@/hooks/useVersion";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
interface LocalLoaderResp {
|
||||
authMode: "password" | "noPassword" | null;
|
||||
|
|
@ -123,7 +124,7 @@ export default function KvmIdRoute() {
|
|||
|
||||
const params = useParams() as { id: string };
|
||||
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
||||
const [ queryParams, setQueryParams ] = useSearchParams();
|
||||
const [queryParams, setQueryParams] = useSearchParams();
|
||||
|
||||
const {
|
||||
peerConnection, setPeerConnection,
|
||||
|
|
@ -145,7 +146,7 @@ export default function KvmIdRoute() {
|
|||
const navigate = useNavigate();
|
||||
const { otaState, setOtaState, setModalView } = useUpdateStore();
|
||||
|
||||
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
||||
const [loadingMessage, setLoadingMessage] = useState(m.connecting_to_device());
|
||||
const cleanupAndStopReconnecting = useCallback(
|
||||
function cleanupAndStopReconnecting() {
|
||||
console.log("Closing peer connection");
|
||||
|
|
@ -182,12 +183,12 @@ export default function KvmIdRoute() {
|
|||
pc: RTCPeerConnection,
|
||||
remoteDescription: RTCSessionDescriptionInit,
|
||||
) {
|
||||
setLoadingMessage("Setting remote description");
|
||||
setLoadingMessage(m.setting_remote_description());
|
||||
|
||||
try {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
||||
console.log("[setRemoteSessionDescription] Remote description set successfully");
|
||||
setLoadingMessage("Establishing secure connection...");
|
||||
setLoadingMessage(m.establishing_secure_connection());
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[setRemoteSessionDescription] Failed to set remote description:",
|
||||
|
|
@ -206,7 +207,7 @@ export default function KvmIdRoute() {
|
|||
if (pc.sctp?.state === "connected") {
|
||||
console.log("[setRemoteSessionDescription] Remote description set");
|
||||
clearInterval(checkInterval);
|
||||
setLoadingMessage("Connection established");
|
||||
setLoadingMessage(m.connection_established());
|
||||
} else if (attempts >= 10) {
|
||||
console.warn(
|
||||
"[setRemoteSessionDescription] Failed to establish connection after 10 attempts",
|
||||
|
|
@ -243,31 +244,32 @@ export default function KvmIdRoute() {
|
|||
retryOnError: true,
|
||||
reconnectAttempts: 15,
|
||||
reconnectInterval: 1000,
|
||||
onReconnectStop: () => {
|
||||
console.debug("Reconnect stopped");
|
||||
onReconnectStop: (numAttempts: number) => {
|
||||
console.debug("Reconnect stopped", numAttempts);
|
||||
cleanupAndStopReconnecting();
|
||||
},
|
||||
|
||||
shouldReconnect(event) {
|
||||
shouldReconnect(event: WebSocketEventMap['close']) {
|
||||
console.debug("[Websocket] shouldReconnect", event);
|
||||
// TODO: Why true?
|
||||
return true;
|
||||
},
|
||||
|
||||
onClose(event) {
|
||||
onClose(event: WebSocketEventMap['close']) {
|
||||
console.debug("[Websocket] onClose", event);
|
||||
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||
},
|
||||
|
||||
onError(event) {
|
||||
onError(event: WebSocketEventMap['error']) {
|
||||
console.error("[Websocket] onError", event);
|
||||
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||
},
|
||||
onOpen() {
|
||||
console.debug("[Websocket] onOpen");
|
||||
onOpen(event: WebSocketEventMap['open']) {
|
||||
console.debug("[Websocket] onOpen", event);
|
||||
},
|
||||
|
||||
onMessage: message => {
|
||||
onMessage(event: WebSocketEventMap['message']) {
|
||||
const message = event as MessageEvent;
|
||||
if (message.data === "pong") return;
|
||||
|
||||
/*
|
||||
|
|
@ -360,12 +362,12 @@ export default function KvmIdRoute() {
|
|||
const sd = btoa(JSON.stringify(pc.localDescription));
|
||||
|
||||
// Legacy mode == UI in cloud with updated code connecting to older device version.
|
||||
// In device mode, old devices wont server this JS, and on newer devices legacy mode wont be enabled
|
||||
// In device mode, old devices wont serve this JS, and on newer devices legacy mode wont be enabled
|
||||
const sessionUrl = `${CLOUD_API}/webrtc/session`;
|
||||
|
||||
console.log("Trying to get remote session description");
|
||||
setLoadingMessage(
|
||||
`Getting remote session description... ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`,
|
||||
m.getting_remote_session_description({ attempt: signalingAttempts.current + 1 }),
|
||||
);
|
||||
const res = await api.POST(sessionUrl, {
|
||||
sd,
|
||||
|
|
@ -382,7 +384,7 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
|
||||
console.debug("Successfully got Remote Session Description. Setting.");
|
||||
setLoadingMessage("Setting remote session description...");
|
||||
setLoadingMessage(m.setting_remote_session_description());
|
||||
|
||||
const decodedSd = atob(json.sd);
|
||||
const parsedSd = JSON.parse(decodedSd);
|
||||
|
|
@ -394,12 +396,12 @@ export default function KvmIdRoute() {
|
|||
const setupPeerConnection = useCallback(async () => {
|
||||
console.debug("[setupPeerConnection] Setting up peer connection");
|
||||
setConnectionFailed(false);
|
||||
setLoadingMessage("Connecting to device...");
|
||||
setLoadingMessage(m.connecting_to_device());
|
||||
|
||||
let pc: RTCPeerConnection;
|
||||
try {
|
||||
console.debug("[setupPeerConnection] Creating peer connection");
|
||||
setLoadingMessage("Creating peer connection...");
|
||||
setLoadingMessage(m.creating_peer_connection());
|
||||
pc = new RTCPeerConnection({
|
||||
// We only use STUN or TURN servers if we're in the cloud
|
||||
...(isInCloud && iceConfig?.iceServers
|
||||
|
|
@ -409,7 +411,7 @@ export default function KvmIdRoute() {
|
|||
|
||||
setPeerConnectionState(pc.connectionState);
|
||||
console.debug("[setupPeerConnection] Peer connection created", pc);
|
||||
setLoadingMessage("Setting up connection to device...");
|
||||
setLoadingMessage(m.setting_up_connection_to_device());
|
||||
} catch (e) {
|
||||
console.error(`[setupPeerConnection] Error creating peer connection: ${e}`);
|
||||
setTimeout(() => {
|
||||
|
|
@ -459,7 +461,7 @@ export default function KvmIdRoute() {
|
|||
const pc = event.currentTarget as RTCPeerConnection;
|
||||
if (pc.iceGatheringState === "complete") {
|
||||
console.debug("ICE Gathering completed");
|
||||
setLoadingMessage("ICE Gathering completed");
|
||||
setLoadingMessage(m.ice_gathering_completed());
|
||||
|
||||
if (isLegacySignalingEnabled.current) {
|
||||
// We can now start the https/ws connection to get the remote session description from the KVM device
|
||||
|
|
@ -467,7 +469,7 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
} else if (pc.iceGatheringState === "gathering") {
|
||||
console.debug("ICE Gathering Started");
|
||||
setLoadingMessage("Gathering ICE candidates...");
|
||||
setLoadingMessage(m.gathering_ice_candidates());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -478,6 +480,8 @@ export default function KvmIdRoute() {
|
|||
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
|
||||
|
||||
const rpcDataChannel = pc.createDataChannel("rpc");
|
||||
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
|
||||
rpcDataChannel.onerror = (e: Event) => console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||
rpcDataChannel.onopen = () => {
|
||||
setRpcDataChannel(rpcDataChannel);
|
||||
};
|
||||
|
|
@ -597,13 +601,14 @@ export default function KvmIdRoute() {
|
|||
});
|
||||
}, 10000);
|
||||
|
||||
const { setNetworkState} = useNetworkStateStore();
|
||||
const { setNetworkState } = useNetworkStateStore();
|
||||
const { setHdmiState } = useVideoStore();
|
||||
const {
|
||||
keyboardLedState, setKeyboardLedState,
|
||||
keysDownState, setKeysDownState, setUsbState,
|
||||
keysDownState, setKeysDownState,
|
||||
setUsbState,
|
||||
} = useHidStore();
|
||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||
const { setHidRpcDisabled } = useRTCStore();
|
||||
|
||||
const [hasUpdated, setHasUpdated] = useState(false);
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
|
@ -681,7 +686,7 @@ export default function KvmIdRoute() {
|
|||
});
|
||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||
|
||||
const [needLedState, setNeedLedState] = useState(true);
|
||||
const [ needLedState, setNeedLedState ] = useState(true);
|
||||
|
||||
// request keyboard led state from the device
|
||||
useEffect(() => {
|
||||
|
|
@ -737,8 +742,8 @@ export default function KvmIdRoute() {
|
|||
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
||||
|
||||
// System update
|
||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||
const [ kvmTerminal, setKvmTerminal ] = useState<RTCDataChannel | null>(null);
|
||||
const [ serialConsole, setSerialConsole ] = useState<RTCDataChannel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!peerConnection) return;
|
||||
|
|
@ -756,7 +761,7 @@ export default function KvmIdRoute() {
|
|||
if (location.pathname !== "/other-session") navigateTo("/");
|
||||
}, [navigateTo, location.pathname]);
|
||||
|
||||
const { appVersion, getLocalVersion} = useVersion();
|
||||
const { appVersion, getLocalVersion } = useVersion();
|
||||
|
||||
useEffect(() => {
|
||||
if (appVersion) return;
|
||||
|
|
@ -837,7 +842,7 @@ export default function KvmIdRoute() {
|
|||
isLoggedIn={authMode === "password" || !!user}
|
||||
userEmail={user?.email}
|
||||
picture={user?.picture}
|
||||
kvmName={deviceName ?? "JetKVM Device"}
|
||||
kvmName={deviceName ?? m.jetkvm_device()}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
|
|
@ -873,11 +878,11 @@ export default function KvmIdRoute() {
|
|||
</div>
|
||||
|
||||
{kvmTerminal && (
|
||||
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
||||
<Terminal type="kvm" dataChannel={kvmTerminal} title={m.kvm_terminal()} />
|
||||
)}
|
||||
|
||||
{serialConsole && (
|
||||
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
|
||||
<Terminal type="serial" dataChannel={serialConsole} title={m.serial_console()} />
|
||||
)}
|
||||
</FeatureFlagProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { LinkButton } from "@/components/Button";
|
|||
import SimpleNavbar from "@/components/SimpleNavbar";
|
||||
import Container from "@/components/Container";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function DevicesAlreadyAdopted() {
|
||||
return (
|
||||
|
|
@ -14,15 +15,12 @@ export default function DevicesAlreadyAdopted() {
|
|||
<div className="flex items-center justify-center w-full h-full isolate">
|
||||
<div className="max-w-2xl -mt-16 space-y-8">
|
||||
<div className="space-y-4 text-center">
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">Device Already Registered</h1>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">{m.already_adopted_title()}</h1>
|
||||
<p className="text-lg text-slate-600 dark:text-slate-400">
|
||||
This device is currently registered to another user in our cloud
|
||||
dashboard.
|
||||
{m.already_adopted_other_user()}
|
||||
</p>
|
||||
<p className="mt-4 text-lg text-slate-600 dark:text-slate-400">
|
||||
If you're the new owner, please ask the previous owner to de-register
|
||||
the device from their account in the cloud dashboard. If you believe
|
||||
this is an error, contact our support team for assistance.
|
||||
{m.already_adopted_new_owner()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -31,7 +29,7 @@ export default function DevicesAlreadyAdopted() {
|
|||
to="/devices"
|
||||
size="LG"
|
||||
theme="primary"
|
||||
text="Return to Dashboard"
|
||||
text={m.already_adopted_return_to_dashboard()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import { LuMonitorSmartphone } from "react-icons/lu";
|
|||
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
|
||||
import { User } from "@hooks/stores";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import EmptyCard from "@components/EmptyCard";
|
||||
import KvmCard from "@components/KvmCard";
|
||||
import { LinkButton } from "@components/Button";
|
||||
import { User } from "@/hooks/stores";
|
||||
import { checkAuth } from "@/main";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
import { m } from "@localizations/messages";
|
||||
|
||||
interface LoaderData {
|
||||
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
||||
|
|
@ -54,10 +55,10 @@ export default function DevicesRoute() {
|
|||
<div className="mt-8 flex items-center justify-between border-b border-b-slate-800/20 pb-4 dark:border-b-slate-300/20">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-black dark:text-white">
|
||||
Cloud KVMs
|
||||
{m.cloud_kvms()}
|
||||
</h1>
|
||||
<p className="text-base text-slate-700 dark:text-slate-400">
|
||||
Manage your cloud KVMs and connect to them securely.
|
||||
{m.cloud_kvms_description()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,15 +67,15 @@ export default function DevicesRoute() {
|
|||
<div className="max-w-3xl">
|
||||
<EmptyCard
|
||||
IconElm={LuMonitorSmartphone}
|
||||
headline="No devices found"
|
||||
description="You don't have any devices with enabled JetKVM Cloud yet."
|
||||
headline={m.cloud_kvms_no_devices()}
|
||||
description={m.cloud_kvms_no_devices_description()}
|
||||
BtnElm={
|
||||
<LinkButton
|
||||
to="https://jetkvm.com/docs/networking/remote-access"
|
||||
size="SM"
|
||||
theme="primary"
|
||||
TrailingIcon={ArrowRightIcon}
|
||||
text="Learn more"
|
||||
text={m.learn_more()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
import { useState } from "react";
|
||||
import { Form, redirect, useActionData } from "react-router";
|
||||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||
import { useState } from "react";
|
||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||
|
||||
import SimpleNavbar from "@components/SimpleNavbar";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import Container from "@components/Container";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import LogoBlueIcon from "@assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import Container from "@components/Container";
|
||||
import ExtLink from "@components/ExtLink";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import SimpleNavbar from "@components/SimpleNavbar";
|
||||
import { DeviceStatus } from "@routes/welcome-local";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import ExtLink from "../components/ExtLink";
|
||||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
import api from "@/api";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
const loader: LoaderFunction = async () => {
|
||||
const res = await api
|
||||
|
|
@ -42,11 +41,11 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
|||
if (response.ok) {
|
||||
return redirect("/");
|
||||
} else {
|
||||
return { error: "Invalid password" };
|
||||
return { error: m.invalid_password() };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { error: "An error occurred while logging in" };
|
||||
return { error: m.login_error() };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -73,10 +72,10 @@ export default function LoginLocalRoute() {
|
|||
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Welcome back to JetKVM
|
||||
{m.login_welcome_back()}
|
||||
</h1>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400">
|
||||
Enter your password to access your JetKVM.
|
||||
{m.login_enter_password_description()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -84,11 +83,11 @@ export default function LoginLocalRoute() {
|
|||
<Form method="POST" className="mx-auto max-w-sm space-y-4">
|
||||
<div className="space-y-4">
|
||||
<InputFieldWithLabel
|
||||
label="Password"
|
||||
label={m.login_password_label()}
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Enter your password"
|
||||
placeholder={m.login_enter_password()}
|
||||
autoFocus
|
||||
error={actionData?.error}
|
||||
TrailingElm={
|
||||
|
|
@ -116,7 +115,7 @@ export default function LoginLocalRoute() {
|
|||
theme="primary"
|
||||
fullWidth
|
||||
type="submit"
|
||||
text="Log In"
|
||||
text={m.log_in()}
|
||||
textAlign="center"
|
||||
/>
|
||||
|
||||
|
|
@ -125,7 +124,7 @@ export default function LoginLocalRoute() {
|
|||
href="https://jetkvm.com/docs/networking/local-access#reset-password"
|
||||
className="hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
{m.login_forgot_password()}
|
||||
</ExtLink>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useLocation, useSearchParams } from "react-router";
|
||||
|
||||
import { m } from "@localizations/messages.js";
|
||||
import AuthLayout from "@components/AuthLayout";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SignupRoute() {
|
||||
const [sq] = useSearchParams();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { useState } from "react";
|
||||
import { Form, redirect, useActionData } from "react-router";
|
||||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import Container from "@components/Container";
|
||||
import { cx } from "@/cva.config";
|
||||
import LogoBlueIcon from "@assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import Container from "@components/Container";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
|
||||
import { GridCard } from "../components/Card";
|
||||
import { cx } from "../cva.config";
|
||||
import api from "../api";
|
||||
import api from "@/api";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ const loader: LoaderFunction = async () => {
|
|||
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const localAuthMode = formData.get("localAuthMode");
|
||||
if (!localAuthMode) return { error: "Please select an authentication mode" };
|
||||
if (!localAuthMode) return { error: m.auth_authentication_mode() };
|
||||
|
||||
if (localAuthMode === "password") {
|
||||
return redirect("/welcome/password");
|
||||
|
|
@ -41,11 +41,11 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
|||
return redirect("/");
|
||||
} catch (error) {
|
||||
console.error("Error setting authentication mode:", error);
|
||||
return { error: "An error occurred while setting the authentication mode" };
|
||||
return { error: m.auth_authentication_mode_error() };
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "Invalid authentication mode" };
|
||||
return { error: m.auth_authentication_mode_invalid() };
|
||||
};
|
||||
|
||||
export default function WelcomeLocalModeRoute() {
|
||||
|
|
@ -75,10 +75,10 @@ export default function WelcomeLocalModeRoute() {
|
|||
style={{ animationDelay: "200ms" }}
|
||||
>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Local Authentication Method
|
||||
{m.auth_mode_local()}
|
||||
</h1>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400">
|
||||
Select how you{"'"}d like to secure your JetKVM device locally.
|
||||
{m.auth_mode_local_description()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -101,12 +101,12 @@ export default function WelcomeLocalModeRoute() {
|
|||
>
|
||||
<div className="space-y-0 text-center">
|
||||
<h3 className="text-base font-bold text-black dark:text-white">
|
||||
{mode === "password" ? "Password protected" : "No Password"}
|
||||
{mode === "password" ? m.auth_mode_local_password() : m.auth_mode_local_no_password()}
|
||||
</h3>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
{mode === "password"
|
||||
? "Secure your device with a password for added protection."
|
||||
: "Quick access without password authentication."}
|
||||
? m.auth_mode_local_password_description()
|
||||
: m.auth_mode_local_no_password_description()}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -142,7 +142,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
theme="primary"
|
||||
fullWidth
|
||||
type="submit"
|
||||
text="Continue"
|
||||
text={m.continue()}
|
||||
textAlign="center"
|
||||
disabled={!selectedMode}
|
||||
/>
|
||||
|
|
@ -153,7 +153,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
className="animate-fadeIn mx-auto max-w-md text-center text-xs text-slate-500 opacity-0 dark:text-slate-400"
|
||||
style={{ animationDelay: "600ms" }}
|
||||
>
|
||||
You can always change your authentication method later in the settings.
|
||||
{m.auth_mode_local_change_later()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { useState, useRef, useEffect } from "react";
|
||||
import { Form, redirect, useActionData } from "react-router";
|
||||
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||
|
||||
import LogoBlueIcon from "@assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import Container from "@components/Container";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import api from "@/api";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
|||
const confirmPassword = formData.get("confirmPassword");
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return { error: "Passwords do not match" };
|
||||
return { error: m.auth_mode_local_password_do_not_match() };
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -43,11 +43,11 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
|||
if (response.ok) {
|
||||
return redirect("/");
|
||||
} else {
|
||||
return { error: "Failed to set password" };
|
||||
return { error: m.auth_mode_local_password_failed_set({ error: response.statusText }) };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error setting password:", error);
|
||||
return { error: "An error occurred while setting the password" };
|
||||
return { error: m.auth_mode_local_password_failed_set({ error: String(error) })};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -86,10 +86,10 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
style={{ animationDelay: "200ms" }}
|
||||
>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Set a Password
|
||||
{m.auth_mode_local_password_set()}
|
||||
</h1>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400">
|
||||
Create a strong password to secure your JetKVM device locally.
|
||||
{m.auth_mode_local_password_set_description()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -101,10 +101,10 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
style={{ animationDelay: "400ms" }}
|
||||
>
|
||||
<InputFieldWithLabel
|
||||
label="Password"
|
||||
label={m.auth_mode_local_password()}
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
placeholder="Enter a password"
|
||||
placeholder={m.auth_mode_local_password_set_label()}
|
||||
autoComplete="new-password"
|
||||
ref={passwordInputRef}
|
||||
TrailingElm={
|
||||
|
|
@ -131,17 +131,17 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
style={{ animationDelay: "400ms" }}
|
||||
>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm Password"
|
||||
label={m.auth_mode_local_password_confirm_label()}
|
||||
autoComplete="new-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm your password"
|
||||
placeholder={m.auth_mode_local_password_confirm_description()}
|
||||
error={actionData?.error}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actionData?.error && <p className="text-sm text-red-600">{}</p>}
|
||||
{actionData?.error && <p className="text-sm text-red-600">{ }</p>}
|
||||
|
||||
<div
|
||||
className="animate-fadeIn opacity-0"
|
||||
|
|
@ -152,7 +152,7 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
theme="primary"
|
||||
fullWidth
|
||||
type="submit"
|
||||
text="Set Password"
|
||||
text={m.auth_mode_local_password_set_button()}
|
||||
textAlign="center"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -163,9 +163,7 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
className="animate-fadeIn max-w-md text-center text-xs text-slate-500 opacity-0 dark:text-slate-400"
|
||||
style={{ animationDelay: "800ms" }}
|
||||
>
|
||||
This password will be used to secure your device data and protect against
|
||||
unauthorized access.{" "}
|
||||
<span className="font-bold">All data remains on your local device.</span>
|
||||
{m.auth_mode_local_password_note()} <span className="font-bold">{m.auth_mode_local_password_note_local()}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ import { redirect } from "react-router";
|
|||
import type { LoaderFunction } from "react-router";
|
||||
import { cx } from "cva";
|
||||
|
||||
import api from "@/api";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import Container from "@components/Container";
|
||||
import { LinkButton } from "@components/Button";
|
||||
import LogoBlueIcon from "@assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@assets/logo-white.svg";
|
||||
import DeviceImage from "@assets/jetkvm-device-still.png";
|
||||
import LogoMark from "@assets/logo-mark.png";
|
||||
import Container from "@components/Container";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import { LinkButton } from "@components/Button";
|
||||
import api from "@/api";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export interface DeviceStatus {
|
||||
|
|
@ -54,17 +54,17 @@ export default function WelcomeRoute() {
|
|||
/>
|
||||
<img
|
||||
src={LogoBlueIcon}
|
||||
alt="JetKVM Logo"
|
||||
alt={m.jetkvm_logo()}
|
||||
className="h-[32px] dark:hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="animate-fadeIn animation-delay-1500 space-y-1 opacity-0">
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Welcome to JetKVM
|
||||
{m.welcome_to_jetkvm()}
|
||||
</h1>
|
||||
<p className="text-lg font-medium text-slate-600 dark:text-slate-400">
|
||||
Control any computer remotely
|
||||
{m.welcome_to_jetkvm_description()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -72,7 +72,7 @@ export default function WelcomeRoute() {
|
|||
<div className="-mt-2! -ml-6 flex items-center justify-center">
|
||||
<img
|
||||
src={DeviceImage}
|
||||
alt="JetKVM Device"
|
||||
alt={m.jetkvm_device()}
|
||||
className="animation-delay-300 animate-fadeInScaleFloat max-w-md scale-[0.98] opacity-0 transition-all duration-1000 ease-out"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -82,14 +82,13 @@ export default function WelcomeRoute() {
|
|||
style={{ animationDelay: "2000ms" }}
|
||||
className="animate-fadeIn mx-auto max-w-lg text-lg text-slate-700 opacity-0 dark:text-slate-300"
|
||||
>
|
||||
JetKVM combines powerful hardware with intuitive software to provide a
|
||||
seamless remote control experience.
|
||||
{m.jetkvm_description()}
|
||||
</p>
|
||||
<div className="animate-fadeIn animation-delay-2300 opacity-0">
|
||||
<LinkButton
|
||||
size="LG"
|
||||
theme="light"
|
||||
text="Set up your JetKVM"
|
||||
text={m.jetkvm_setup()}
|
||||
LeadingIcon={({ className }) => (
|
||||
<img src={LogoMark} className={cx(className, "mr-1.5 h-5!")} />
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue