mirror of https://github.com/jetkvm/kvm.git
Compare commits
7 Commits
5a2d948192
...
13407d2bd8
| Author | SHA1 | Date |
|---|---|---|
|
|
13407d2bd8 | |
|
|
1647b80b8c | |
|
|
214bd69d10 | |
|
|
e39fdd9c7d | |
|
|
340a04f23a | |
|
|
f2e665126a | |
|
|
cc9ff74276 |
|
|
@ -104,6 +104,7 @@ type Config struct {
|
|||
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
||||
NetworkConfig *network.NetworkConfig `json:"network_config"`
|
||||
DefaultLogLevel string `json:"default_log_level"`
|
||||
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
||||
}
|
||||
|
||||
func (c *Config) GetDisplayRotation() uint16 {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type Native struct {
|
|||
onVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
onIndevEvent func(event string)
|
||||
onRpcEvent func(event string)
|
||||
sleepModeSupported bool
|
||||
videoLock sync.Mutex
|
||||
screenLock sync.Mutex
|
||||
}
|
||||
|
|
@ -62,6 +63,8 @@ func NewNative(opts NativeOptions) *Native {
|
|||
}
|
||||
}
|
||||
|
||||
sleepModeSupported := isSleepModeSupported()
|
||||
|
||||
return &Native{
|
||||
ready: make(chan struct{}),
|
||||
l: nativeLogger,
|
||||
|
|
@ -73,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
|
|||
onVideoFrameReceived: onVideoFrameReceived,
|
||||
onIndevEvent: onIndevEvent,
|
||||
onRpcEvent: onRpcEvent,
|
||||
sleepModeSupported: sleepModeSupported,
|
||||
videoLock: sync.Mutex{},
|
||||
screenLock: sync.Mutex{},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
||||
|
||||
// VideoState is the state of the video stream.
|
||||
type VideoState struct {
|
||||
Ready bool `json:"ready"`
|
||||
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
|
||||
|
|
@ -8,6 +15,58 @@ type VideoState struct {
|
|||
FramePerSecond float64 `json:"fps"`
|
||||
}
|
||||
|
||||
func isSleepModeSupported() bool {
|
||||
_, err := os.Stat(sleepModeFile)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (n *Native) setSleepMode(enabled bool) error {
|
||||
if !n.sleepModeSupported {
|
||||
return nil
|
||||
}
|
||||
|
||||
bEnabled := "0"
|
||||
if enabled {
|
||||
bEnabled = "1"
|
||||
}
|
||||
return os.WriteFile(sleepModeFile, []byte(bEnabled), 0644)
|
||||
}
|
||||
|
||||
func (n *Native) getSleepMode() (bool, error) {
|
||||
if !n.sleepModeSupported {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(sleepModeFile)
|
||||
if err == nil {
|
||||
return string(data) == "1", nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// VideoSetSleepMode sets the sleep mode for the video stream.
|
||||
func (n *Native) VideoSetSleepMode(enabled bool) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return n.setSleepMode(enabled)
|
||||
}
|
||||
|
||||
// VideoGetSleepMode gets the sleep mode for the video stream.
|
||||
func (n *Native) VideoGetSleepMode() (bool, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return n.getSleepMode()
|
||||
}
|
||||
|
||||
// VideoSleepModeSupported checks if the sleep mode is supported.
|
||||
func (n *Native) VideoSleepModeSupported() bool {
|
||||
return n.sleepModeSupported
|
||||
}
|
||||
|
||||
// VideoSetQualityFactor sets the quality factor for the video stream.
|
||||
func (n *Native) VideoSetQualityFactor(factor float64) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -15,6 +74,7 @@ func (n *Native) VideoSetQualityFactor(factor float64) error {
|
|||
return videoSetStreamQualityFactor(factor)
|
||||
}
|
||||
|
||||
// VideoGetQualityFactor gets the quality factor for the video stream.
|
||||
func (n *Native) VideoGetQualityFactor() (float64, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -22,6 +82,7 @@ func (n *Native) VideoGetQualityFactor() (float64, error) {
|
|||
return videoGetStreamQualityFactor()
|
||||
}
|
||||
|
||||
// VideoSetEDID sets the EDID for the video stream.
|
||||
func (n *Native) VideoSetEDID(edid string) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -29,6 +90,7 @@ func (n *Native) VideoSetEDID(edid string) error {
|
|||
return videoSetEDID(edid)
|
||||
}
|
||||
|
||||
// VideoGetEDID gets the EDID for the video stream.
|
||||
func (n *Native) VideoGetEDID() (string, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -36,6 +98,7 @@ func (n *Native) VideoGetEDID() (string, error) {
|
|||
return videoGetEDID()
|
||||
}
|
||||
|
||||
// VideoLogStatus gets the log status for the video stream.
|
||||
func (n *Native) VideoLogStatus() (string, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -43,6 +106,7 @@ func (n *Native) VideoLogStatus() (string, error) {
|
|||
return videoLogStatus(), nil
|
||||
}
|
||||
|
||||
// VideoStop stops the video stream.
|
||||
func (n *Native) VideoStop() error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -51,10 +115,14 @@ func (n *Native) VideoStop() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// VideoStart starts the video stream.
|
||||
func (n *Native) VideoStart() error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
// disable sleep mode before starting video
|
||||
_ = n.setSleepMode(false)
|
||||
|
||||
videoStart()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1215,6 +1215,8 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getEDID": {Func: rpcGetEDID},
|
||||
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
|
||||
"getVideoLogStatus": {Func: rpcGetVideoLogStatus},
|
||||
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
|
||||
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
|
||||
"getDevChannelState": {Func: rpcGetDevChannelState},
|
||||
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
|
||||
"getLocalVersion": {Func: rpcGetLocalVersion},
|
||||
|
|
|
|||
3
main.go
3
main.go
|
|
@ -77,6 +77,9 @@ func Main() {
|
|||
// initialize display
|
||||
initDisplay()
|
||||
|
||||
// start video sleep mode timer
|
||||
startVideoSleepModeTicker()
|
||||
|
||||
go func() {
|
||||
time.Sleep(15 * time.Minute)
|
||||
for {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,52 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"access_adopt_kvm": "Adopt KVM to Cloud",
|
||||
"access_adopted_message": "Your device is adopted to the Cloud",
|
||||
"access_auth_mode_no_password": "Current mode: No password",
|
||||
"access_auth_mode_password": "Current mode: Password protected",
|
||||
"access_authentication_mode_title": "Authentication Mode",
|
||||
"access_certificate_label": "Certificate",
|
||||
"access_change_password_button": "Change Password",
|
||||
"access_change_password_description": "Update your device access password",
|
||||
"access_change_password_title": "Change Password",
|
||||
"access_cloud_api_url_label": "Cloud API URL",
|
||||
"access_cloud_app_url_label": "Cloud Application URL",
|
||||
"access_cloud_provider_description": "Select the cloud provider for your device",
|
||||
"access_cloud_provider_title": "Cloud Provider",
|
||||
"access_cloud_security_title": "Cloud Security",
|
||||
"access_confirm_deregister": "Are you sure you want to de-register this device?",
|
||||
"access_deregister": "De-register from Cloud",
|
||||
"access_description": "Manage the Access Control of the device",
|
||||
"access_disable_protection": "Disable Protection",
|
||||
"access_enable_password": "Enable Password",
|
||||
"access_failed_deregister": "Failed to de-register device: {error}",
|
||||
"access_failed_update_cloud_url": "Failed to update cloud URL: {error}",
|
||||
"access_failed_update_tls": "Failed to update TLS settings: {error}",
|
||||
"access_github_link": "GitHub",
|
||||
"access_https_description": "Configure secure HTTPS access to your device",
|
||||
"access_https_mode_title": "HTTPS Mode",
|
||||
"access_learn_security": "Learn about our cloud security",
|
||||
"access_local_description": "Manage the mode of local access to the device",
|
||||
"access_local_title": "Local",
|
||||
"access_no_device_id": "No device ID available",
|
||||
"access_private_key_description": "For security reasons, it will not be displayed after saving.",
|
||||
"access_private_key_label": "Private Key",
|
||||
"access_provider_custom": "Custom",
|
||||
"access_provider_jetkvm": "JetKVM Cloud",
|
||||
"access_remote_description": "Manage the mode of Remote access to the device",
|
||||
"access_security_encryption": "End-to-end encryption using WebRTC (DTLS and SRTP)",
|
||||
"access_security_oidc": "OIDC (OpenID Connect) authentication",
|
||||
"access_security_open_source": "All cloud components are open-source and available on GitHub.",
|
||||
"access_security_streams": "All streams encrypted in transit",
|
||||
"access_security_zero_trust": "Zero Trust security model",
|
||||
"access_title": "Access",
|
||||
"access_tls_certificate_description": "Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates).",
|
||||
"access_tls_certificate_title": "TLS Certificate",
|
||||
"access_tls_custom": "Custom",
|
||||
"access_tls_disabled": "Disabled",
|
||||
"access_tls_self_signed": "Self-signed",
|
||||
"access_tls_updated": "TLS settings updated successfully",
|
||||
"access_update_tls_settings": "Update TLS Settings",
|
||||
"action_bar_connection_stats": "Connection Stats",
|
||||
"action_bar_exit_fullscreen": "Exit Fullscreen",
|
||||
"action_bar_extension": "Extension",
|
||||
|
|
@ -10,10 +57,62 @@
|
|||
"action_bar_virtual_media": "Virtual Media",
|
||||
"action_bar_wake_on_lan": "Wake on LAN",
|
||||
"action_bar_web_terminal": "Web Terminal",
|
||||
"advanced_description": "Access additional settings for troubleshooting and customization",
|
||||
"advanced_dev_channel_description": "Receive early updates from the development channel",
|
||||
"advanced_dev_channel_title": "Dev Channel Updates",
|
||||
"advanced_developer_mode_description": "Enable advanced features for developers",
|
||||
"advanced_developer_mode_enabled_title": "Developer Mode Enabled",
|
||||
"advanced_developer_mode_title": "Developer Mode",
|
||||
"advanced_developer_mode_warning_advanced": "For advanced users only. Not for production use.",
|
||||
"advanced_developer_mode_warning_risks": "Only use if you understand the risks",
|
||||
"advanced_developer_mode_warning_security": "Security is weakened while active",
|
||||
"advanced_disable_usb_emulation": "Disable USB Emulation",
|
||||
"advanced_enable_usb_emulation": "Enable USB Emulation",
|
||||
"advanced_error_loopback_disable": "Failed to disable loopback-only mode: {error}",
|
||||
"advanced_error_loopback_enable": "Failed to enable loopback-only mode: {error}",
|
||||
"advanced_error_reset_config": "Failed to reset configuration: {error}",
|
||||
"advanced_error_set_dev_channel": "Failed to set dev channel state: {error}",
|
||||
"advanced_error_set_dev_mode": "Failed to set dev mode: {error}",
|
||||
"advanced_error_update_ssh_key": "Failed to update SSH key: {error}",
|
||||
"advanced_error_usb_emulation_disable": "Failed to disable USB emulation: {error}",
|
||||
"advanced_error_usb_emulation_enable": "Failed to enable USB emulation: {error}",
|
||||
"advanced_loopback_only_description": "Restrict web interface access to localhost only (127.0.0.1)",
|
||||
"advanced_loopback_only_title": "Loopback-Only Mode",
|
||||
"advanced_loopback_warning_before": "Before enabling this feature, make sure you have either:",
|
||||
"advanced_loopback_warning_cloud": "Cloud access enabled and working",
|
||||
"advanced_loopback_warning_confirm": "I Understand, Enable Anyway",
|
||||
"advanced_loopback_warning_description": "WARNING: This will restrict web interface access to localhost (127.0.0.1) only.",
|
||||
"advanced_loopback_warning_ssh": "SSH access configured and tested",
|
||||
"advanced_loopback_warning_title": "Enable Loopback-Only Mode?",
|
||||
"advanced_reset_config_button": "Reset Config",
|
||||
"advanced_reset_config_description": "Reset configuration to default. This will log you out.",
|
||||
"advanced_reset_config_title": "Reset Configuration",
|
||||
"advanced_ssh_access_description": "Add your SSH public key to enable secure remote access to the device",
|
||||
"advanced_ssh_access_title": "SSH Access",
|
||||
"advanced_ssh_default_user": "The default SSH user is",
|
||||
"advanced_ssh_public_key_label": "SSH Public Key",
|
||||
"advanced_ssh_public_key_placeholder": "Enter your SSH public key",
|
||||
"advanced_success_loopback_disabled": "Loopback-only mode disabled. Restart your device to apply.",
|
||||
"advanced_success_loopback_enabled": "Loopback-only mode enabled. Restart your device to apply.",
|
||||
"advanced_success_reset_config": "Configuration reset to default successfully",
|
||||
"advanced_success_update_ssh_key": "SSH key updated successfully",
|
||||
"advanced_title": "Advanced",
|
||||
"advanced_troubleshooting_mode_description": "Diagnostic tools and additional controls for troubleshooting and development purposes",
|
||||
"advanced_troubleshooting_mode_title": "Troubleshooting Mode",
|
||||
"advanced_update_ssh_key_button": "Update SSH Key",
|
||||
"advanced_usb_emulation_description": "Control the USB emulation state",
|
||||
"advanced_usb_emulation_title": "USB Emulation",
|
||||
"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",
|
||||
"appearance_description": "Choose your preferred color theme",
|
||||
"appearance_page_description": "Customize the look and feel of your JetKVM interface",
|
||||
"appearance_theme_dark": "Dark",
|
||||
"appearance_theme_light": "Light",
|
||||
"appearance_theme_system": "System",
|
||||
"appearance_theme": "Theme",
|
||||
"appearance_title": "Appearance",
|
||||
"attach": "Attach",
|
||||
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
|
||||
"atx_power_control_hdd_led": "HDD LED",
|
||||
|
|
@ -86,7 +185,7 @@
|
|||
"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...",
|
||||
"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}",
|
||||
|
|
@ -137,8 +236,77 @@
|
|||
"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...",
|
||||
"gathering_ice_candidates": "Gathering ICE candidates…",
|
||||
"general_app_version": "App: {version}",
|
||||
"general_auto_update_description": "Automatically update the device to the latest version",
|
||||
"general_auto_update_error": "Failed to set auto-update: {error}",
|
||||
"general_auto_update_title": "Auto Update",
|
||||
"general_check_for_updates": "Check for Updates",
|
||||
"general_page_description": "Configure device settings and update preferences",
|
||||
"general_reboot_description": "Do you want to proceed with rebooting the system?",
|
||||
"general_reboot_device_description": "Power cycle the JetKVM",
|
||||
"general_reboot_device": "Reboot Device",
|
||||
"general_reboot_no_button": "No",
|
||||
"general_reboot_title": "Reboot JetKVM",
|
||||
"general_reboot_yes_button": "Yes",
|
||||
"general_system_version": "System: {version}",
|
||||
"general_title": "General",
|
||||
"general_update_app_update_title": "App Update",
|
||||
"general_update_application_type": "App",
|
||||
"general_update_available_description": "A new update is available to enhance system performance and improve compatibility. We recommend updating to ensure everything runs smoothly.",
|
||||
"general_update_available_title": "Update available",
|
||||
"general_update_background_button": "Update in Background",
|
||||
"general_update_check_again_button": "Check Again",
|
||||
"general_update_checking_description": "We're ensuring your device has the latest features and improvements.",
|
||||
"general_update_checking_title": "Checking for updates…",
|
||||
"general_update_completed_description": "Your device has been successfully updated to the latest version. Enjoy the new features and improvements!",
|
||||
"general_update_completed_title": "Update Completed Successfully",
|
||||
"general_update_error_description": "An error occurred while updating your device. Please try again later.",
|
||||
"general_update_error_details": "Error details: {errorMessage}",
|
||||
"general_update_error_title": "Update Error",
|
||||
"general_update_later_button": "Do it later",
|
||||
"general_update_now_button": "Update Now",
|
||||
"general_update_rebooting": "Rebooting to complete the update…",
|
||||
"general_update_status_awaiting_reboot": "Awaiting reboot",
|
||||
"general_update_status_downloading": "Downloading {update_type} update…",
|
||||
"general_update_status_fetching": "Fetching update information…",
|
||||
"general_update_status_installing": "Installing {update_type} update…",
|
||||
"general_update_status_verifying": "Verifying {update_type} update…",
|
||||
"general_update_system_type": "System",
|
||||
"general_update_system_update_title": "Linux System Update",
|
||||
"general_update_up_to_date_description": "Your system is running the latest version. No updates are currently available.",
|
||||
"general_update_up_to_date_title": "System is up to date",
|
||||
"general_update_updating_description": "Please don't turn off your device. This process may take a few minutes.",
|
||||
"general_update_updating_title": "Updating your device",
|
||||
"getting_remote_session_description": "Getting remote session description attempt {attempt}",
|
||||
"hardware_backlight_settings_error": "Failed to set backlight settings: {error}",
|
||||
"hardware_backlight_settings_get_error": "Failed to get backlight settings: {error}",
|
||||
"hardware_backlight_settings_success": "Backlight settings updated successfully",
|
||||
"hardware_dim_display_after_description": "Set how long to wait before dimming the display",
|
||||
"hardware_dim_display_after_title": "Dim Display After",
|
||||
"hardware_display_brightness_description": "Set the brightness of the display",
|
||||
"hardware_display_brightness_high": "High",
|
||||
"hardware_display_brightness_low": "Low",
|
||||
"hardware_display_brightness_medium": "Medium",
|
||||
"hardware_display_brightness_off": "Off",
|
||||
"hardware_display_brightness_title": "Display Brightness",
|
||||
"hardware_display_orientation_description": "Set the orientation of the display",
|
||||
"hardware_display_orientation_error": "Failed to set display orientation: {error}",
|
||||
"hardware_display_orientation_inverted": "Inverted",
|
||||
"hardware_display_orientation_normal": "Normal",
|
||||
"hardware_display_orientation_success": "Display orientation updated successfully",
|
||||
"hardware_display_orientation_title": "Display Orientation",
|
||||
"hardware_display_wake_up_note": "The display will wake up when the connection state changes, or when touched.",
|
||||
"hardware_page_description": "Configure display settings and hardware options for your JetKVM device",
|
||||
"hardware_time_1_hour": "1 Hour",
|
||||
"hardware_time_1_minute": "1 Minute",
|
||||
"hardware_time_10_minutes": "10 Minutes",
|
||||
"hardware_time_30_minutes": "30 Minutes",
|
||||
"hardware_time_5_minutes": "5 Minutes",
|
||||
"hardware_time_never": "Never",
|
||||
"hardware_title": "Hardware",
|
||||
"hardware_turn_off_display_after_description": "Period of inactivity before display automatically turns off",
|
||||
"hardware_turn_off_display_after_title": "Turn off Display After",
|
||||
"hide": "Hide",
|
||||
"ice_gathering_completed": "ICE Gathering completed",
|
||||
"info_caps_lock": "Caps Lock",
|
||||
|
|
@ -185,11 +353,59 @@
|
|||
"jiggler_save_jiggler_config": "Save Jiggler Config",
|
||||
"jiggler_timezone_description": "Timezone for cron schedule",
|
||||
"jiggler_timezone_label": "Timezone",
|
||||
"keyboard_description": "Configure keyboard settings for your device",
|
||||
"keyboard_layout_description": "Keyboard layout of target operating system",
|
||||
"keyboard_layout_error": "Failed to set keyboard layout: {error}",
|
||||
"keyboard_layout_long_description": "The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.",
|
||||
"keyboard_layout_success": "Keyboard layout set successfully to {layout}",
|
||||
"keyboard_layout_title": "Keyboard Layout",
|
||||
"keyboard_show_pressed_keys_description": "Display currently pressed keys in the status bar",
|
||||
"keyboard_show_pressed_keys_title": "Show Pressed Keys",
|
||||
"keyboard_title": "Keyboard",
|
||||
"kvm_terminal": "KVM Terminal",
|
||||
"last_online": "Last online {time}",
|
||||
"learn_more": "Learn more",
|
||||
"load": "Load",
|
||||
"loading": "Loading…",
|
||||
"local_auth_change_local_device_password_description": "Enter your current password and a new password to update your local device protection.",
|
||||
"local_auth_change_local_device_password_title": "Change Local Device Password",
|
||||
"local_auth_confirm_new_password_label": "Confirm New Password",
|
||||
"local_auth_create_confirm_password_label": "Confirm New Password",
|
||||
"local_auth_create_confirm_password_placeholder": "Re-enter your password",
|
||||
"local_auth_create_description": "Create a password to protect your device from unauthorized local access.",
|
||||
"local_auth_create_new_password_label": "New Password",
|
||||
"local_auth_create_new_password_placeholder": "Enter a strong password",
|
||||
"local_auth_create_not_now_button": "Not Now",
|
||||
"local_auth_create_secure_button": "Secure Device",
|
||||
"local_auth_create_title": "Local Device Protection",
|
||||
"local_auth_current_password_label": "Current Password",
|
||||
"local_auth_disable_local_device_protection_description": "Enter your current password to disable local device protection.",
|
||||
"local_auth_disable_local_device_protection_title": "Disable Local Device Protection",
|
||||
"local_auth_disable_protection_button": "Disable Protection",
|
||||
"local_auth_enter_current_password_placeholder": "Enter your current password",
|
||||
"local_auth_enter_new_password_placeholder": "Enter a new strong password",
|
||||
"local_auth_error_changing_password": "An error occurred while changing the password",
|
||||
"local_auth_error_disabling_password": "An error occurred while disabling the password",
|
||||
"local_auth_error_enter_current_password": "Please enter your current password",
|
||||
"local_auth_error_enter_new_password": "Please enter a new password",
|
||||
"local_auth_error_enter_old_password": "Please enter your old password",
|
||||
"local_auth_error_enter_password": "Please enter a password",
|
||||
"local_auth_error_passwords_not_match": "Passwords do not match",
|
||||
"local_auth_error_setting_password": "An error occurred while setting the password",
|
||||
"local_auth_new_password_label": "New Password",
|
||||
"local_auth_reenter_new_password_placeholder": "Re-enter your new password",
|
||||
"local_auth_success_password_disabled_description": "You've successfully disabled the password protection for local access. Remember, your device is now less secure.",
|
||||
"local_auth_success_password_disabled_title": "Password Protection Disabled",
|
||||
"local_auth_success_password_set_description": "You've successfully set up local device protection. Your device is now secure against unauthorized local access.",
|
||||
"local_auth_success_password_set_title": "Password Set Successfully",
|
||||
"local_auth_success_password_updated_description": "You've successfully changed your local device protection password. Make sure to remember your new password for future access.",
|
||||
"local_auth_success_password_updated_title": "Password Updated Successfully",
|
||||
"local_auth_update_confirm_password_label": "Confirm New Password",
|
||||
"local_auth_update_current_password_label": "Current Password",
|
||||
"local_auth_update_description": "Enter your current password and a new password to update your local device protection.",
|
||||
"local_auth_update_new_password_label": "New Password",
|
||||
"local_auth_update_password_button": "Update Password",
|
||||
"local_auth_update_title": "Change Local Device Password",
|
||||
"log_in": "Log In",
|
||||
"log_out": "Log out",
|
||||
"logged_in_as": "Logged in as",
|
||||
|
|
@ -222,6 +438,54 @@
|
|||
"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",
|
||||
"macros_add_description": "Create a new keyboard macro",
|
||||
"macros_add_new": "Add New Macro",
|
||||
"macros_create_first": "Create your first macro to get started",
|
||||
"macros_created_success": "Macro \"{name}\" created successfully",
|
||||
"macros_delete_confirm": "Are you sure you want to delete this macro? This action cannot be undone.",
|
||||
"macros_delete_macro": "Delete Macro",
|
||||
"macros_deleted_success": "Macro \"{name}\" deleted successfully",
|
||||
"macros_deleting": "Deleting",
|
||||
"macros_duplicate": "Duplicate",
|
||||
"macros_duplicated_success": "Macro \"{name}\" duplicated successfully",
|
||||
"macros_edit_description": "Modify your keyboard macro",
|
||||
"macros_edit_title": "Edit Macro",
|
||||
"macros_edit": "Edit",
|
||||
"macros_failed_create": "Failed to create macro",
|
||||
"macros_failed_create_error": "Failed to create macro: {error}",
|
||||
"macros_failed_delete": "Failed to delete macro",
|
||||
"macros_failed_delete_error": "Failed to delete macro: {error}",
|
||||
"macros_failed_duplicate": "Failed to duplicate macro",
|
||||
"macros_failed_duplicate_error": "Failed to duplicate macro: {error}",
|
||||
"macros_failed_reorder": "Failed to reorder macros",
|
||||
"macros_failed_reorder_error": "Failed to reorder macros: {error}",
|
||||
"macros_failed_update": "Failed to update macro",
|
||||
"macros_failed_update_error": "Failed to update macro: {error}",
|
||||
"macros_invalid_data": "Invalid macro data",
|
||||
"macros_maximum_macros_reached": "You have reached the maximum number of {maximum} macros allowed.",
|
||||
"macros_move_down": "Move Down",
|
||||
"macros_move_up": "Move Up",
|
||||
"macros_no_macros_available": "No macros available",
|
||||
"macros_no_macros_found": "No macros found",
|
||||
"macros_order_updated": "Macro order updated successfully",
|
||||
"macros_title": "Keyboard Macros",
|
||||
"macros_updated_success": "Macro \"{name}\" updated successfully",
|
||||
"macros_aria_delete": "Delete macro {name}",
|
||||
"macros_aria_duplicate": "Duplicate macro {name}",
|
||||
"macros_aria_edit": "Edit macro {name}",
|
||||
"macros_aria_move_down": "Move {name} down",
|
||||
"macros_aria_move_up": "Move {name} up",
|
||||
"macros_confirm_delete_description": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
||||
"macros_confirm_delete_title": "Delete Macro",
|
||||
"macros_confirm_deleting": "Deleting…",
|
||||
"macros_add_new_macro": "Add New Macro",
|
||||
"macros_aria_add_new": "Add new macro",
|
||||
"macros_create_first_headline": "Create Your First Macro",
|
||||
"macros_create_first_description": "Combine keystrokes into a single action",
|
||||
"macros_delay_only": "Delay only",
|
||||
"macros_edit_button": "Edit",
|
||||
"macros_loading": "Loading macros…",
|
||||
"macros_max_reached": "Max Reached",
|
||||
"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",
|
||||
|
|
@ -337,6 +601,7 @@
|
|||
"rename_device_new_name_placeholder": "Plex Media Server",
|
||||
"rename_device_no_name": "Please specify a name",
|
||||
"rename": "Rename",
|
||||
"retry": "Retry",
|
||||
"saving": "Saving…",
|
||||
"search_placeholder": "Search…",
|
||||
"serial_console_baud_rate": "Baud Rate",
|
||||
|
|
@ -457,139 +722,5 @@
|
|||
"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",
|
||||
|
||||
"access_adopt_kvm": "Adopt KVM to Cloud",
|
||||
"access_adopted_message": "Your device is adopted to the Cloud",
|
||||
"access_auth_mode_no_password": "Current mode: No password",
|
||||
"access_auth_mode_password": "Current mode: Password protected",
|
||||
"access_authentication_mode_title": "Authentication Mode",
|
||||
"access_certificate_label": "Certificate",
|
||||
"access_change_password_button": "Change Password",
|
||||
"access_change_password_description": "Update your device access password",
|
||||
"access_change_password_title": "Change Password",
|
||||
"access_cloud_api_url_label": "Cloud API URL",
|
||||
"access_cloud_app_url_label": "Cloud Application URL",
|
||||
"access_cloud_provider_description": "Select the cloud provider for your device",
|
||||
"access_cloud_provider_title": "Cloud Provider",
|
||||
"access_cloud_security_title": "Cloud Security",
|
||||
"access_confirm_deregister": "Are you sure you want to de-register this device?",
|
||||
"access_deregister": "De-register from Cloud",
|
||||
"access_description": "Manage the Access Control of the device",
|
||||
"access_disable_protection": "Disable Protection",
|
||||
"access_enable_password": "Enable Password",
|
||||
"access_failed_deregister": "Failed to de-register device: {error}",
|
||||
"access_failed_update_cloud_url": "Failed to update cloud URL: {error}",
|
||||
"access_failed_update_tls": "Failed to update TLS settings: {error}",
|
||||
"access_github_link": "GitHub",
|
||||
"access_https_description": "Configure secure HTTPS access to your device",
|
||||
"access_https_mode_title": "HTTPS Mode",
|
||||
"access_learn_security": "Learn about our cloud security",
|
||||
"access_local_description": "Manage the mode of local access to the device",
|
||||
"access_local_title": "Local",
|
||||
"access_no_device_id": "No device ID available",
|
||||
"access_private_key_description": "For security reasons, it will not be displayed after saving.",
|
||||
"access_private_key_label": "Private Key",
|
||||
"access_provider_custom": "Custom",
|
||||
"access_provider_jetkvm": "JetKVM Cloud",
|
||||
"access_remote_description": "Manage the mode of Remote access to the device",
|
||||
"access_security_encryption": "End-to-end encryption using WebRTC (DTLS and SRTP)",
|
||||
"access_security_oidc": "OIDC (OpenID Connect) authentication",
|
||||
"access_security_open_source": "All cloud components are open-source and available on GitHub.",
|
||||
"access_security_streams": "All streams encrypted in transit",
|
||||
"access_security_zero_trust": "Zero Trust security model",
|
||||
"access_title": "Access",
|
||||
"access_tls_certificate_description": "Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates).",
|
||||
"access_tls_certificate_title": "TLS Certificate",
|
||||
"access_tls_custom": "Custom",
|
||||
"access_tls_disabled": "Disabled",
|
||||
"access_tls_self_signed": "Self-signed",
|
||||
"access_tls_updated": "TLS settings updated successfully",
|
||||
"access_update_tls_settings": "Update TLS Settings",
|
||||
|
||||
"local_auth_change_local_device_password_description": "Enter your current password and a new password to update your local device protection.",
|
||||
"local_auth_change_local_device_password_title": "Change Local Device Password",
|
||||
"local_auth_confirm_new_password_label": "Confirm New Password",
|
||||
"local_auth_create_confirm_password_label": "Confirm New Password",
|
||||
"local_auth_create_confirm_password_placeholder": "Re-enter your password",
|
||||
"local_auth_create_description": "Create a password to protect your device from unauthorized local access.",
|
||||
"local_auth_create_new_password_label": "New Password",
|
||||
"local_auth_create_new_password_placeholder": "Enter a strong password",
|
||||
"local_auth_create_not_now_button": "Not Now",
|
||||
"local_auth_create_secure_button": "Secure Device",
|
||||
"local_auth_create_title": "Local Device Protection",
|
||||
"local_auth_current_password_label": "Current Password",
|
||||
"local_auth_disable_local_device_protection_description": "Enter your current password to disable local device protection.",
|
||||
"local_auth_disable_local_device_protection_title": "Disable Local Device Protection",
|
||||
"local_auth_disable_protection_button": "Disable Protection",
|
||||
"local_auth_enter_current_password_placeholder": "Enter your current password",
|
||||
"local_auth_enter_new_password_placeholder": "Enter a new strong password",
|
||||
"local_auth_error_changing_password": "An error occurred while changing the password",
|
||||
"local_auth_error_disabling_password": "An error occurred while disabling the password",
|
||||
"local_auth_error_enter_current_password": "Please enter your current password",
|
||||
"local_auth_error_enter_new_password": "Please enter a new password",
|
||||
"local_auth_error_enter_old_password": "Please enter your old password",
|
||||
"local_auth_error_enter_password": "Please enter a password",
|
||||
"local_auth_error_passwords_not_match": "Passwords do not match",
|
||||
"local_auth_error_setting_password": "An error occurred while setting the password",
|
||||
"local_auth_new_password_label": "New Password",
|
||||
"local_auth_reenter_new_password_placeholder": "Re-enter your new password",
|
||||
"local_auth_success_password_disabled_description": "You've successfully disabled the password protection for local access. Remember, your device is now less secure.",
|
||||
"local_auth_success_password_disabled_title": "Password Protection Disabled",
|
||||
"local_auth_success_password_set_description": "You've successfully set up local device protection. Your device is now secure against unauthorized local access.",
|
||||
"local_auth_success_password_set_title": "Password Set Successfully",
|
||||
"local_auth_success_password_updated_description": "You've successfully changed your local device protection password. Make sure to remember your new password for future access.",
|
||||
"local_auth_success_password_updated_title": "Password Updated Successfully",
|
||||
"local_auth_update_confirm_password_label": "Confirm New Password",
|
||||
"local_auth_update_current_password_label": "Current Password",
|
||||
"local_auth_update_description": "Enter your current password and a new password to update your local device protection.",
|
||||
"local_auth_update_new_password_label": "New Password",
|
||||
"local_auth_update_password_button": "Update Password",
|
||||
"local_auth_update_title": "Change Local Device Password",
|
||||
|
||||
"advanced_description": "Access additional settings for troubleshooting and customization",
|
||||
"advanced_dev_channel_description": "Receive early updates from the development channel",
|
||||
"advanced_dev_channel_title": "Dev Channel Updates",
|
||||
"advanced_developer_mode_description": "Enable advanced features for developers",
|
||||
"advanced_developer_mode_enabled_title": "Developer Mode Enabled",
|
||||
"advanced_developer_mode_title": "Developer Mode",
|
||||
"advanced_developer_mode_warning_advanced": "For advanced users only. Not for production use.",
|
||||
"advanced_developer_mode_warning_risks": "Only use if you understand the risks",
|
||||
"advanced_developer_mode_warning_security": "Security is weakened while active",
|
||||
"advanced_disable_usb_emulation": "Disable USB Emulation",
|
||||
"advanced_enable_usb_emulation": "Enable USB Emulation",
|
||||
"advanced_error_loopback_disable": "Failed to disable loopback-only mode: {error}",
|
||||
"advanced_error_loopback_enable": "Failed to enable loopback-only mode: {error}",
|
||||
"advanced_error_reset_config": "Failed to reset configuration: {error}",
|
||||
"advanced_error_set_dev_channel": "Failed to set dev channel state: {error}",
|
||||
"advanced_error_set_dev_mode": "Failed to set dev mode: {error}",
|
||||
"advanced_error_update_ssh_key": "Failed to update SSH key: {error}",
|
||||
"advanced_error_usb_emulation_disable": "Failed to disable USB emulation: {error}",
|
||||
"advanced_error_usb_emulation_enable": "Failed to enable USB emulation: {error}",
|
||||
"advanced_loopback_only_description": "Restrict web interface access to localhost only (127.0.0.1)",
|
||||
"advanced_loopback_only_title": "Loopback-Only Mode",
|
||||
"advanced_loopback_warning_before": "Before enabling this feature, make sure you have either:",
|
||||
"advanced_loopback_warning_cloud": "Cloud access enabled and working",
|
||||
"advanced_loopback_warning_confirm": "I Understand, Enable Anyway",
|
||||
"advanced_loopback_warning_description": "WARNING: This will restrict web interface access to localhost (127.0.0.1) only.",
|
||||
"advanced_loopback_warning_ssh": "SSH access configured and tested",
|
||||
"advanced_loopback_warning_title": "Enable Loopback-Only Mode?",
|
||||
"advanced_reset_config_button": "Reset Config",
|
||||
"advanced_reset_config_description": "Reset configuration to default. This will log you out.",
|
||||
"advanced_reset_config_title": "Reset Configuration",
|
||||
"advanced_ssh_access_description": "Add your SSH public key to enable secure remote access to the device",
|
||||
"advanced_ssh_access_title": "SSH Access",
|
||||
"advanced_ssh_default_user": "The default SSH user is",
|
||||
"advanced_ssh_public_key_label": "SSH Public Key",
|
||||
"advanced_ssh_public_key_placeholder": "Enter your SSH public key",
|
||||
"advanced_success_loopback_disabled": "Loopback-only mode disabled. Restart your device to apply.",
|
||||
"advanced_success_loopback_enabled": "Loopback-only mode enabled. Restart your device to apply.",
|
||||
"advanced_success_reset_config": "Configuration reset to default successfully",
|
||||
"advanced_success_update_ssh_key": "SSH key updated successfully",
|
||||
"advanced_title": "Advanced",
|
||||
"advanced_troubleshooting_mode_description": "Diagnostic tools and additional controls for troubleshooting and development purposes",
|
||||
"advanced_troubleshooting_mode_title": "Troubleshooting Mode",
|
||||
"advanced_update_ssh_key_button": "Update SSH Key",
|
||||
"advanced_usb_emulation_description": "Control the USB emulation state",
|
||||
"advanced_usb_emulation_title": "USB Emulation"
|
||||
"welcome_to_jetkvm": "Welcome to JetKVM"
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useCallback, useState } from "react";
|
||||
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsAppearanceRoute() {
|
||||
const [currentTheme, setCurrentTheme] = useState(() => {
|
||||
|
|
@ -28,22 +28,24 @@ export default function SettingsAppearanceRoute() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const themeOptions = [
|
||||
{ value: "system", label: m.appearance_theme_system() },
|
||||
{ value: "light", label: m.appearance_theme_light() },
|
||||
{ value: "dark", label: m.appearance_theme_dark() },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Appearance"
|
||||
description="Customize the look and feel of your JetKVM interface"
|
||||
title={m.appearance_title()}
|
||||
description={m.appearance_page_description()}
|
||||
/>
|
||||
<SettingsItem title="Theme" description="Choose your preferred color theme">
|
||||
<SettingsItem title={m.appearance_theme()} description={m.appearance_description()}>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={currentTheme}
|
||||
options={[
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
]}
|
||||
options={themeOptions}
|
||||
onChange={e => {
|
||||
setCurrentTheme(e.target.value);
|
||||
handleThemeChange(e.target.value);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
import { useState , useEffect } from "react";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { useDeviceStore } from "@hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import Checkbox from "@components/Checkbox";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { Button } from "../components/Button";
|
||||
import notifications from "../notifications";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
import { useDeviceStore } from "../hooks/stores";
|
||||
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsGeneralRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
|
|
@ -34,7 +32,7 @@ export default function SettingsGeneralRoute() {
|
|||
send("setAutoUpdateState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set auto-update: ${resp.error.data || "Unknown error"}`,
|
||||
m.general_auto_update_error({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -45,44 +43,36 @@ export default function SettingsGeneralRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="General"
|
||||
description="Configure device settings and update preferences"
|
||||
title={m.general_title()}
|
||||
description={m.general_page_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 pb-2">
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title="Check for Updates"
|
||||
title={m.general_check_for_updates()}
|
||||
description={
|
||||
currentVersions ? (
|
||||
<>
|
||||
App: {currentVersions.appVersion}
|
||||
{m.general_app_version({ version: currentVersions ? currentVersions.appVersion : m.loading() })}
|
||||
<br />
|
||||
System: {currentVersions.systemVersion}
|
||||
{m.general_system_version({ version: currentVersions ? currentVersions.systemVersion : m.loading() })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
App: Loading...
|
||||
<br />
|
||||
System: Loading...
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Check for Updates"
|
||||
text={m.general_check_for_updates()}
|
||||
onClick={() => navigateTo("./update")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Auto Update"
|
||||
description="Automatically update the device to the latest version"
|
||||
title={m.general_auto_update_title()}
|
||||
description={m.general_auto_update_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={autoUpdate}
|
||||
|
|
@ -95,14 +85,14 @@ export default function SettingsGeneralRoute() {
|
|||
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title="Reboot Device"
|
||||
description="Power cycle the JetKVM"
|
||||
title={m.general_reboot_device()}
|
||||
description={m.general_reboot_device_description()}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reboot Device"
|
||||
text={m.general_reboot_device()}
|
||||
onClick={() => navigateTo("./reboot")}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useNavigate } from "react-router";
|
|||
|
||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -50,15 +51,15 @@ function ConfirmationBox({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Reboot JetKVM
|
||||
{m.general_reboot_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Do you want to proceed with rebooting the system?
|
||||
{m.general_reboot_description()}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||
<Button size="SM" theme="light" text={m.general_reboot_yes_button()} onClick={onYes} />
|
||||
<Button size="SM" theme="blank" text={m.general_reboot_no_button()} onClick={onNo} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { UpdateState, useUpdateStore } from "@hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { SystemVersionInfo, useVersion } from "@hooks/useVersion";
|
||||
import { Button } from "@components/Button";
|
||||
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { SystemVersionInfo, useVersion } from "@/hooks/useVersion";
|
||||
import Card from "@components/Card";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsGeneralUpdateRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -160,10 +161,10 @@ function LoadingState({
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-0">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Checking for updates...
|
||||
{m.general_update_checking_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
We{"'"}re ensuring your device has the latest features and improvements.
|
||||
{m.general_update_checking_description()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300">
|
||||
|
|
@ -174,7 +175,7 @@ function LoadingState({
|
|||
></div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancelCheck} />
|
||||
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancelCheck} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -228,16 +229,18 @@ function UpdatingDeviceState({
|
|||
const verfiedAt = otaState[`${type}VerifiedAt`];
|
||||
const updatedAt = otaState[`${type}UpdatedAt`];
|
||||
|
||||
const update_type = () => (type === "system" ? m.general_update_system_type() : m.general_update_app_type());
|
||||
|
||||
if (!otaState.metadataFetchedAt) {
|
||||
return "Fetching update information...";
|
||||
return m.general_update_status_fetching();
|
||||
} else if (!downloadFinishedAt) {
|
||||
return `Downloading ${type} update...`;
|
||||
return m.general_update_status_downloading({ update_type });
|
||||
} else if (!verfiedAt) {
|
||||
return `Verifying ${type} update...`;
|
||||
return m.general_update_status_verifying({ update_type });
|
||||
} else if (!updatedAt) {
|
||||
return `Installing ${type} update...`;
|
||||
return m.general_update_status_installing({ update_type });
|
||||
} else {
|
||||
return `Awaiting reboot`;
|
||||
return m.general_update_status_awaiting_reboot();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -260,10 +263,10 @@ function UpdatingDeviceState({
|
|||
<div className="w-full max-w-sm space-y-4">
|
||||
<div className="space-y-0">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Updating your device
|
||||
{m.general_update_updating_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Please don{"'"}t turn off your device. This process may take a few minutes.
|
||||
{m.general_update_updating_description()}
|
||||
</p>
|
||||
</div>
|
||||
<Card className="space-y-4 p-4">
|
||||
|
|
@ -272,7 +275,7 @@ function UpdatingDeviceState({
|
|||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Rebooting to complete the update...
|
||||
{m.general_update_rebooting()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -288,7 +291,7 @@ function UpdatingDeviceState({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-black dark:text-white">
|
||||
Linux System Update
|
||||
{m.general_update_system_update_title()}
|
||||
</p>
|
||||
{calculateOverallProgress("system") < 100 ? (
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
|
|
@ -320,7 +323,7 @@ function UpdatingDeviceState({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-black dark:text-white">
|
||||
App Update
|
||||
{m.general_update_app_update_title()}
|
||||
</p>
|
||||
{calculateOverallProgress("app") < 100 ? (
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
|
|
@ -352,7 +355,7 @@ function UpdatingDeviceState({
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Update in Background"
|
||||
text={m.general_update_background_button()}
|
||||
onClick={onMinimizeUpgradeDialog}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -372,15 +375,15 @@ function SystemUpToDateState({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
System is up to date
|
||||
{m.general_update_up_to_date_title()}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Your system is running the latest version. No updates are currently available.
|
||||
{m.general_update_up_to_date_description()}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<Button size="SM" theme="light" text="Check Again" onClick={checkUpdate} />
|
||||
<Button size="SM" theme="blank" text="Back" onClick={onClose} />
|
||||
<Button size="SM" theme="light" text={m.general_update_check_again_button()} onClick={checkUpdate} />
|
||||
<Button size="SM" theme="blank" text={m.general_update_back_button()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -400,30 +403,27 @@ function UpdateAvailableState({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Update available
|
||||
{m.general_update_available_title()}
|
||||
</p>
|
||||
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
A new update is available to enhance system performance and improve
|
||||
compatibility. We recommend updating to ensure everything runs smoothly.
|
||||
{m.general_update_available_description()}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
||||
{versionInfo?.systemUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">System:</span>{" "}
|
||||
{versionInfo?.remote?.systemVersion}
|
||||
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.remote?.systemVersion}
|
||||
<br />
|
||||
</>
|
||||
) : null}
|
||||
{versionInfo?.appUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">App:</span>{" "}
|
||||
{versionInfo?.remote?.appVersion}
|
||||
<span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.remote?.appVersion}
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
<div className="flex items-center justify-start gap-x-2">
|
||||
<Button size="SM" theme="primary" text="Update Now" onClick={onConfirmUpdate} />
|
||||
<Button size="SM" theme="light" text="Do it later" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.general_update_now_button()} onClick={onConfirmUpdate} />
|
||||
<Button size="SM" theme="light" text={m.general_update_later_button()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -435,14 +435,13 @@ function UpdateCompletedState({ onClose }: { onClose: () => void }) {
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold dark:text-white">
|
||||
Update Completed Successfully
|
||||
{m.general_update_completed_title()}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
Your device has been successfully updated to the latest version. Enjoy the new
|
||||
features and improvements!
|
||||
{m.general_update_completed_description()}
|
||||
</p>
|
||||
<div className="flex items-center justify-start">
|
||||
<Button size="SM" theme="primary" text="Back" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.back()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -461,18 +460,18 @@ function UpdateErrorState({
|
|||
return (
|
||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold dark:text-white">Update Error</p>
|
||||
<p className="text-base font-semibold dark:text-white">{m.general_update_error_title()}</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
An error occurred while updating your device. Please try again later.
|
||||
{m.general_update_error_description()}
|
||||
</p>
|
||||
{errorMessage && (
|
||||
<p className="mb-4 text-sm font-medium text-red-600 dark:text-red-400">
|
||||
Error details: {errorMessage}
|
||||
{m.general_update_error_details({ errorMessage })}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-start gap-x-2">
|
||||
<Button size="SM" theme="light" text="Back" onClick={onClose} />
|
||||
<Button size="SM" theme="blank" text="Retry" onClick={onRetryUpdate} />
|
||||
<Button size="SM" theme="light" text={m.back()} onClick={onClose} />
|
||||
<Button size="SM" theme="blank" text={m.retry()} onClick={onRetryUpdate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { UsbInfoSetting } from "@components/UsbInfoSetting";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
|
|
@ -24,11 +25,11 @@ export default function SettingsHardwareRoute() {
|
|||
send("setDisplayRotation", { params: { rotation: displayRotation } }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
|
||||
m.hardware_display_orientation_error({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Display orientation updated successfully");
|
||||
notifications.success(m.hardware_display_orientation_success());
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -49,11 +50,11 @@ export default function SettingsHardwareRoute() {
|
|||
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||
m.hardware_backlight_settings_error({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Backlight settings updated successfully");
|
||||
notifications.success(m.hardware_backlight_settings_success());
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ export default function SettingsHardwareRoute() {
|
|||
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
return notifications.error(
|
||||
`Failed to get backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||
m.hardware_backlight_settings_get_error({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
}
|
||||
const result = resp.result as BacklightSettings;
|
||||
|
|
@ -87,21 +88,21 @@ export default function SettingsHardwareRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Hardware"
|
||||
description="Configure display settings and hardware options for your JetKVM device"
|
||||
title={m.hardware_title()}
|
||||
description={m.hardware_page_description()}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Display Orientation"
|
||||
description="Set the orientation of the display"
|
||||
title={m.hardware_display_orientation_title()}
|
||||
description={m.hardware_display_orientation_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.displayRotation.toString()}
|
||||
options={[
|
||||
{ value: "270", label: "Normal" },
|
||||
{ value: "90", label: "Inverted" },
|
||||
{ value: "270", label: m.hardware_display_orientation_normal() },
|
||||
{ value: "90", label: m.hardware_display_orientation_inverted() },
|
||||
]}
|
||||
onChange={e => {
|
||||
handleDisplayRotationChange(e.target.value);
|
||||
|
|
@ -109,18 +110,18 @@ export default function SettingsHardwareRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Display Brightness"
|
||||
description="Set the brightness of the display"
|
||||
title={m.hardware_display_brightness_title()}
|
||||
description={m.hardware_display_brightness_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={backlightSettings.max_brightness.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Off" },
|
||||
{ value: "10", label: "Low" },
|
||||
{ value: "35", label: "Medium" },
|
||||
{ value: "64", label: "High" },
|
||||
{ value: "0", label: m.hardware_display_brightness_off() },
|
||||
{ value: "10", label: m.hardware_display_brightness_low() },
|
||||
{ value: "35", label: m.hardware_display_brightness_medium() },
|
||||
{ value: "64", label: m.hardware_display_brightness_high() },
|
||||
]}
|
||||
onChange={e => {
|
||||
handleBacklightMaxBrightnessChange(parseInt(e.target.value));
|
||||
|
|
@ -130,20 +131,20 @@ export default function SettingsHardwareRoute() {
|
|||
{backlightSettings.max_brightness != 0 && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="Dim Display After"
|
||||
description="Set how long to wait before dimming the display"
|
||||
title={m.hardware_dim_display_after_title()}
|
||||
description={m.hardware_dim_display_after_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={backlightSettings.dim_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "60", label: "1 Minute" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
{ value: "0", label: m.hardware_time_never() },
|
||||
{ value: "60", label: m.hardware_time_1_minute() },
|
||||
{ value: "300", label: m.hardware_time_5_minutes() },
|
||||
{ value: "600", label: m.hardware_time_10_minutes() },
|
||||
{ value: "1800", label: m.hardware_time_30_minutes() },
|
||||
{ value: "3600", label: m.hardware_time_1_hour() },
|
||||
]}
|
||||
onChange={e => {
|
||||
handleBacklightDimAfterChange(parseInt(e.target.value));
|
||||
|
|
@ -151,19 +152,19 @@ export default function SettingsHardwareRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Turn off Display After"
|
||||
description="Period of inactivity before display automatically turns off"
|
||||
title={m.hardware_turn_off_display_after_title()}
|
||||
description={m.hardware_turn_off_display_after_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={backlightSettings.off_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
{ value: "0", label: m.hardware_time_never() },
|
||||
{ value: "300", label: m.hardware_time_5_minutes() },
|
||||
{ value: "600", label: m.hardware_time_10_minutes() },
|
||||
{ value: "1800", label: m.hardware_time_30_minutes() },
|
||||
{ value: "3600", label: m.hardware_time_1_hour() },
|
||||
]}
|
||||
onChange={e => {
|
||||
handleBacklightOffAfterChange(parseInt(e.target.value));
|
||||
|
|
@ -173,7 +174,7 @@ export default function SettingsHardwareRoute() {
|
|||
</>
|
||||
)}
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The display will wake up when the connection state changes, or when touched.
|
||||
{m.hardware_display_wake_up_note()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
import { useSettingsStore } from "@hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsKeyboardRoute() {
|
||||
const { setKeyboardLayout } = useSettingsStore();
|
||||
|
|
@ -33,10 +34,10 @@ export default function SettingsKeyboardRoute() {
|
|||
send("setKeyboardLayout", { layout: isoCode }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
|
||||
m.keyboard_layout_error({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
}
|
||||
notifications.success("Keyboard layout set successfully to " + isoCode);
|
||||
notifications.success(m.keyboard_layout_success({ layout: isoCode }));
|
||||
setKeyboardLayout(isoCode);
|
||||
});
|
||||
},
|
||||
|
|
@ -46,14 +47,14 @@ export default function SettingsKeyboardRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Keyboard"
|
||||
description="Configure keyboard settings for your device"
|
||||
title={m.keyboard_title()}
|
||||
description={m.keyboard_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Keyboard Layout"
|
||||
description="Keyboard layout of target operating system"
|
||||
title={m.keyboard_layout_title()}
|
||||
description={m.keyboard_layout_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
|
|
@ -65,14 +66,14 @@ export default function SettingsKeyboardRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
||||
{m.keyboard_layout_long_description()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Show Pressed Keys"
|
||||
description="Display currently pressed keys in the status bar"
|
||||
title={m.keyboard_show_pressed_keys_title()}
|
||||
description={m.keyboard_show_pressed_keys_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={showPressedKeys}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import { useNavigate } from "react-router";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import { MacroForm } from "@/components/MacroForm";
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@hooks/stores";
|
||||
import { MacroForm } from "@components/MacroForm";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { DEFAULT_DELAY } from "@/constants/macros";
|
||||
import notifications from "@/notifications";
|
||||
import { normalizeSortOrders } from "@/utils";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsMacrosAddRoute() {
|
||||
const { macros, saveMacros } = useMacrosStore();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddMacro = async (macro: Partial<KeySequence>) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
|
|
@ -30,13 +25,13 @@ export default function SettingsMacrosAddRoute() {
|
|||
};
|
||||
|
||||
await saveMacros(normalizeSortOrders([...macros, newMacro]));
|
||||
notifications.success(`Macro "${newMacro.name}" created successfully`);
|
||||
notifications.success(m.macros_created_success({name: newMacro.name}));
|
||||
navigate("../");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to create macro: ${error.message}`);
|
||||
notifications.error(m.macros_failed_create_error({error: error.message || m.unknown_error() }));
|
||||
} else {
|
||||
notifications.error("Failed to create macro");
|
||||
notifications.error(m.macros_failed_create());
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
|
|
@ -46,8 +41,8 @@ export default function SettingsMacrosAddRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Add New Macro"
|
||||
description="Create a new keyboard macro"
|
||||
title={m.macros_add_new()}
|
||||
description={m.macros_add_description()}
|
||||
/>
|
||||
<MacroForm
|
||||
initialData={{
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
import { useNavigate, useParams } from "react-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { LuTrash2 } from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import { MacroForm } from "@/components/MacroForm";
|
||||
import { KeySequence, useMacrosStore } from "@hooks/stores";
|
||||
import { Button } from "@components/Button";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
import { MacroForm } from "@components/MacroForm";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import notifications from "@/notifications";
|
||||
import { Button } from "@/components/Button";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
};
|
||||
import { normalizeSortOrders } from "@/utils";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsMacrosEditRoute() {
|
||||
const { macros, saveMacros } = useMacrosStore();
|
||||
|
|
@ -56,13 +51,13 @@ export default function SettingsMacrosEditRoute() {
|
|||
);
|
||||
|
||||
await saveMacros(normalizeSortOrders(newMacros));
|
||||
notifications.success(`Macro "${updatedMacro.name}" updated successfully`);
|
||||
notifications.success(m.macros_updated_success({ name: updatedMacro.name }));
|
||||
navigate("../");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to update macro: ${error.message}`);
|
||||
notifications.error(m.macros_failed_update({ error: error.message }));
|
||||
} else {
|
||||
notifications.error("Failed to update macro");
|
||||
notifications.error(m.macros_failed_update());
|
||||
}
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
|
|
@ -76,13 +71,13 @@ export default function SettingsMacrosEditRoute() {
|
|||
try {
|
||||
const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macro.id));
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success(`Macro "${macro.name}" deleted successfully`);
|
||||
notifications.success(m.macros_deleted_success({ name: macro.name }));
|
||||
navigate("../macros");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to delete macro: ${error.message}`);
|
||||
notifications.error(m.macros_failed_delete_error({ error: error.message }));
|
||||
} else {
|
||||
notifications.error("Failed to delete macro");
|
||||
notifications.error(m.macros_failed_delete());
|
||||
}
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
|
|
@ -95,13 +90,13 @@ export default function SettingsMacrosEditRoute() {
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<SettingsPageHeader
|
||||
title="Edit Macro"
|
||||
description="Modify your keyboard macro"
|
||||
title={m.macros_edit_title()}
|
||||
description={m.macros_edit_description()}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Delete Macro"
|
||||
|
||||
className="text-red-500 dark:text-red-400"
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
|
|
@ -118,10 +113,10 @@ export default function SettingsMacrosEditRoute() {
|
|||
<ConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onClose={() => setShowDeleteConfirm(false)}
|
||||
title="Delete Macro"
|
||||
description="Are you sure you want to delete this macro? This action cannot be undone."
|
||||
title={m.macros_delete_macro()}
|
||||
description={m.macros_delete_confirm()}
|
||||
variant="danger"
|
||||
confirmText={isDeleting ? "Deleting" : "Delete"}
|
||||
confirmText={isDeleting ? m.macros_deleting() : m.delete()}
|
||||
onConfirm={() => {
|
||||
handleDeleteMacro();
|
||||
setShowDeleteConfirm(false);
|
||||
|
|
|
|||
|
|
@ -11,23 +11,18 @@ import {
|
|||
LuCommand,
|
||||
} from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import { Button } from "@/components/Button";
|
||||
import EmptyCard from "@/components/EmptyCard";
|
||||
import Card from "@/components/Card";
|
||||
import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros";
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@hooks/stores";
|
||||
import useKeyboardLayout from "@hooks/useKeyboardLayout";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { Button } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
import EmptyCard from "@components/EmptyCard";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import notifications from "@/notifications";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
};
|
||||
import { normalizeSortOrders } from "@/utils";
|
||||
import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsMacrosRoute() {
|
||||
const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore();
|
||||
|
|
@ -51,12 +46,12 @@ export default function SettingsMacrosRoute() {
|
|||
const handleDuplicateMacro = useCallback(
|
||||
async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
notifications.error(m.macros_invalid_data());
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaxMacrosReached) {
|
||||
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
|
||||
notifications.error(m.macros_maximum_macros_reached({ maximum: MAX_TOTAL_MACROS }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -71,12 +66,12 @@ export default function SettingsMacrosRoute() {
|
|||
|
||||
try {
|
||||
await saveMacros(normalizeSortOrders([...macros, newMacroCopy]));
|
||||
notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`);
|
||||
notifications.success(m.macros_duplicated_success({ name: newMacroCopy.name }));
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to duplicate macro: ${error.message}`);
|
||||
notifications.error(m.macros_failed_duplicate_error({ error: error.message || m.unknown_error() }));
|
||||
} else {
|
||||
notifications.error("Failed to duplicate macro");
|
||||
notifications.error(m.macros_failed_duplicate());
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
|
|
@ -88,7 +83,7 @@ export default function SettingsMacrosRoute() {
|
|||
const handleMoveMacro = useCallback(
|
||||
async (index: number, direction: "up" | "down", macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
notifications.error(m.macros_no_macros_available());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -103,12 +98,12 @@ export default function SettingsMacrosRoute() {
|
|||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
notifications.success(m.macros_order_updated());
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
notifications.error(m.macros_failed_reorder_error({ error: error.message || m.unknown_error() }));
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
notifications.error(m.macros_failed_reorder());
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
|
|
@ -126,14 +121,14 @@ export default function SettingsMacrosRoute() {
|
|||
macros.filter(m => m.id !== macroToDelete.id),
|
||||
);
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success(`Macro "${macroToDelete.name}" deleted successfully`);
|
||||
notifications.success(m.macros_deleted_success({ name: macroToDelete.name }));
|
||||
setShowDeleteConfirm(false);
|
||||
setMacroToDelete(null);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to delete macro: ${error.message}`);
|
||||
notifications.error(m.macros_failed_delete_error({ error: error.message || m.unknown_error() }));
|
||||
} else {
|
||||
notifications.error("Failed to delete macro");
|
||||
notifications.error(m.macros_failed_delete());
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
|
|
@ -153,7 +148,7 @@ export default function SettingsMacrosRoute() {
|
|||
onClick={() => handleMoveMacro(index, "up", macro.id)}
|
||||
disabled={index === 0 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowUp}
|
||||
aria-label={`Move ${macro.name} up`}
|
||||
aria-label={m.macros_aria_move_up({ name: macro.name })}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
|
|
@ -161,7 +156,7 @@ export default function SettingsMacrosRoute() {
|
|||
onClick={() => handleMoveMacro(index, "down", macro.id)}
|
||||
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowDown}
|
||||
aria-label={`Move ${macro.name} down`}
|
||||
aria-label={m.macros_aria_move_down({ name: macro.name })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -189,10 +184,7 @@ export default function SettingsMacrosRoute() {
|
|||
{selectedKeyboard.modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
|
@ -201,10 +193,7 @@ export default function SettingsMacrosRoute() {
|
|||
step.modifiers.length > 0 &&
|
||||
Array.isArray(step.keys) &&
|
||||
step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
|
||||
{Array.isArray(step.keys) &&
|
||||
|
|
@ -214,17 +203,14 @@ export default function SettingsMacrosRoute() {
|
|||
{selectedKeyboard.keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">
|
||||
Delay only
|
||||
{m.macros_delay_only()}
|
||||
</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
|
|
@ -251,7 +237,7 @@ export default function SettingsMacrosRoute() {
|
|||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Delete macro ${macro.name}`}
|
||||
aria-label={m.macros_aria_delete({ name: macro.name })}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
|
|
@ -259,16 +245,16 @@ export default function SettingsMacrosRoute() {
|
|||
LeadingIcon={LuCopy}
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
aria-label={m.macros_aria_duplicate({ name: macro.name })}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
text="Edit"
|
||||
text={m.macros_edit_button()}
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Edit macro ${macro.name}`}
|
||||
aria-label={m.macros_aria_edit({ name: macro.name })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -281,10 +267,10 @@ export default function SettingsMacrosRoute() {
|
|||
setShowDeleteConfirm(false);
|
||||
setMacroToDelete(null);
|
||||
}}
|
||||
title="Delete Macro"
|
||||
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
|
||||
title={m.macros_confirm_delete_title()}
|
||||
description={m.macros_confirm_delete_description({ name: macroToDelete?.name || "" })}
|
||||
variant="danger"
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? m.macros_confirm_deleting() : m.macros_delete_confirm_button()}
|
||||
onConfirm={handleDeleteMacro}
|
||||
isConfirming={actionLoadingId === macroToDelete?.id}
|
||||
/>
|
||||
|
|
@ -309,18 +295,18 @@ export default function SettingsMacrosRoute() {
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<SettingsPageHeader
|
||||
title="Keyboard Macros"
|
||||
description={`Combine keystrokes into a single action for faster workflows.`}
|
||||
title={m.macros_title()}
|
||||
description={m.macros_add_new()}
|
||||
/>
|
||||
{macros.length > 0 && (
|
||||
<div className="flex items-center pl-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"}
|
||||
text={isMaxMacrosReached ? m.macros_max_reached() : m.macros_add_new_macro()}
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
aria-label={m.macros_aria_add_new()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -330,7 +316,7 @@ export default function SettingsMacrosRoute() {
|
|||
{loading && macros.length === 0 ? (
|
||||
<EmptyCard
|
||||
IconElm={LuCommand}
|
||||
headline="Loading macros..."
|
||||
headline={m.macros_loading()}
|
||||
BtnElm={
|
||||
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
|
|
@ -340,16 +326,16 @@ export default function SettingsMacrosRoute() {
|
|||
) : macros.length === 0 ? (
|
||||
<EmptyCard
|
||||
IconElm={LuCommand}
|
||||
headline="Create Your First Macro"
|
||||
description="Combine keystrokes into a single action"
|
||||
headline={m.macros_create_first_headline()}
|
||||
description={m.macros_create_first_description()}
|
||||
BtnElm={
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Add New Macro"
|
||||
text={m.macros_add_new_macro()}
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
aria-label={m.macros_aria_add_new()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { KeySequence } from "@hooks/stores";
|
||||
|
||||
export const formatters = {
|
||||
date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
|
||||
new Intl.DateTimeFormat("en-US", {
|
||||
|
|
@ -243,3 +245,10 @@ export function isChromeOS() {
|
|||
/* ChromeOS sets navigator.platform to Linux :/ */
|
||||
return !!navigator.userAgent.match(" CrOS ");
|
||||
}
|
||||
|
||||
export function normalizeSortOrders(macros: KeySequence[]): KeySequence[] {
|
||||
return macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
};
|
||||
|
|
|
|||
103
video.go
103
video.go
|
|
@ -1,10 +1,22 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/native"
|
||||
)
|
||||
|
||||
var lastVideoState native.VideoState
|
||||
var (
|
||||
lastVideoState native.VideoState
|
||||
videoSleepModeCtx context.Context
|
||||
videoSleepModeCancel context.CancelFunc
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVideoSleepModeDuration = 1 * time.Minute
|
||||
)
|
||||
|
||||
func triggerVideoStateUpdate() {
|
||||
go func() {
|
||||
|
|
@ -17,3 +29,92 @@ func triggerVideoStateUpdate() {
|
|||
func rpcGetVideoState() (native.VideoState, error) {
|
||||
return lastVideoState, nil
|
||||
}
|
||||
|
||||
type rpcVideoSleepModeResponse struct {
|
||||
Supported bool `json:"supported"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
func rpcGetVideoSleepMode() rpcVideoSleepModeResponse {
|
||||
sleepMode, _ := nativeInstance.VideoGetSleepMode()
|
||||
return rpcVideoSleepModeResponse{
|
||||
Supported: nativeInstance.VideoSleepModeSupported(),
|
||||
Enabled: sleepMode,
|
||||
Duration: config.VideoSleepAfterSec,
|
||||
}
|
||||
}
|
||||
|
||||
func rpcSetVideoSleepMode(duration int) error {
|
||||
if duration < 0 {
|
||||
duration = -1 // disable
|
||||
}
|
||||
|
||||
config.VideoSleepAfterSec = duration
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
// we won't restart the ticker here,
|
||||
// as the session can't be inactive when this function is called
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopVideoSleepModeTicker() {
|
||||
nativeLogger.Trace().Msg("stopping HDMI sleep mode ticker")
|
||||
|
||||
if videoSleepModeCancel != nil {
|
||||
nativeLogger.Trace().Msg("canceling HDMI sleep mode ticker context")
|
||||
videoSleepModeCancel()
|
||||
videoSleepModeCancel = nil
|
||||
videoSleepModeCtx = nil
|
||||
}
|
||||
}
|
||||
|
||||
func startVideoSleepModeTicker() {
|
||||
if !nativeInstance.VideoSleepModeSupported() {
|
||||
return
|
||||
}
|
||||
|
||||
var duration time.Duration
|
||||
|
||||
if config.VideoSleepAfterSec == 0 {
|
||||
duration = defaultVideoSleepModeDuration
|
||||
} else if config.VideoSleepAfterSec > 0 {
|
||||
duration = time.Duration(config.VideoSleepAfterSec) * time.Second
|
||||
} else {
|
||||
stopVideoSleepModeTicker()
|
||||
return
|
||||
}
|
||||
|
||||
// Stop any existing timer and goroutine
|
||||
stopVideoSleepModeTicker()
|
||||
|
||||
// Create new context for this ticker
|
||||
videoSleepModeCtx, videoSleepModeCancel = context.WithCancel(context.Background())
|
||||
|
||||
go doVideoSleepModeTicker(videoSleepModeCtx, duration)
|
||||
}
|
||||
|
||||
func doVideoSleepModeTicker(ctx context.Context, duration time.Duration) {
|
||||
timer := time.NewTimer(duration)
|
||||
defer timer.Stop()
|
||||
|
||||
nativeLogger.Trace().Msg("HDMI sleep mode ticker started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if getActiveSessions() > 0 {
|
||||
nativeLogger.Warn().Msg("not going to enter HDMI sleep mode because there are active sessions")
|
||||
continue
|
||||
}
|
||||
|
||||
nativeLogger.Trace().Msg("entering HDMI sleep mode")
|
||||
_ = nativeInstance.VideoSetSleepMode(true)
|
||||
case <-ctx.Done():
|
||||
nativeLogger.Trace().Msg("HDMI sleep mode ticker stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
webrtc.go
38
webrtc.go
|
|
@ -39,6 +39,34 @@ type Session struct {
|
|||
keysDownStateQueue chan usbgadget.KeysDownState
|
||||
}
|
||||
|
||||
var (
|
||||
actionSessions int = 0
|
||||
activeSessionsMutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func incrActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
actionSessions++
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func decrActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
actionSessions--
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func getActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func (s *Session) resetKeepAliveTime() {
|
||||
s.keepAliveJitterLock.Lock()
|
||||
defer s.keepAliveJitterLock.Unlock()
|
||||
|
|
@ -312,9 +340,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
if connectionState == webrtc.ICEConnectionStateConnected {
|
||||
if !isConnected {
|
||||
isConnected = true
|
||||
actionSessions++
|
||||
onActiveSessionsChanged()
|
||||
if actionSessions == 1 {
|
||||
if incrActiveSessions() == 1 {
|
||||
onFirstSessionConnected()
|
||||
}
|
||||
}
|
||||
|
|
@ -353,9 +380,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
}
|
||||
if isConnected {
|
||||
isConnected = false
|
||||
actionSessions--
|
||||
onActiveSessionsChanged()
|
||||
if actionSessions == 0 {
|
||||
if decrActiveSessions() == 0 {
|
||||
onLastSessionDisconnected()
|
||||
}
|
||||
}
|
||||
|
|
@ -364,16 +390,16 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
return session, nil
|
||||
}
|
||||
|
||||
var actionSessions = 0
|
||||
|
||||
func onActiveSessionsChanged() {
|
||||
requestDisplayUpdate(true, "active_sessions_changed")
|
||||
}
|
||||
|
||||
func onFirstSessionConnected() {
|
||||
_ = nativeInstance.VideoStart()
|
||||
stopVideoSleepModeTicker()
|
||||
}
|
||||
|
||||
func onLastSessionDisconnected() {
|
||||
_ = nativeInstance.VideoStop()
|
||||
startVideoSleepModeTicker()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue