mirror of https://github.com/jetkvm/kvm.git
Compare commits
40 Commits
70f7ce5b0b
...
88b22a4378
Author | SHA1 | Date |
---|---|---|
|
88b22a4378 | |
|
9ed19a451d | |
|
057f6c9d57 | |
|
8d1475f29a | |
|
9056dea38b | |
|
d8d11a0020 | |
|
0d28a5d914 | |
|
ac42be96e7 | |
|
2feef185c4 | |
|
4cbde51ce7 | |
|
e4ec2c1d8d | |
|
cb1937aac6 | |
|
af9c8fad09 | |
|
dd5eee8179 | |
|
58d59eca47 | |
|
66f8d7bcd6 | |
|
926e0b8a06 | |
|
384025ecf5 | |
|
768a9f7604 | |
|
a255680b8a | |
|
0ae4639275 | |
|
85e3b22660 | |
|
c550948bef | |
|
949be95cd5 | |
|
d54568642b | |
|
c9068af568 | |
|
033bdcd645 | |
|
baf85dcbec | |
|
c9dd3cd926 | |
|
7ccb8e617c | |
|
340babac24 | |
|
2aa7b8569f | |
|
19bd161a7f | |
|
38252de03c | |
|
63c2272c45 | |
|
8ee0532f0e | |
|
d0faf03239 | |
|
77b4c1c531 | |
|
5f8b451cd7 | |
|
5a4f1766b7 |
|
@ -4,7 +4,7 @@
|
|||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
// Should match what is defined in ui/package.json
|
||||
"version": "21.1.0"
|
||||
"version": "22.15.0"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
|
|
|
@ -87,8 +87,10 @@ type Config struct {
|
|||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
|
||||
KeyboardLayout string `json:"keyboard_layout"`
|
||||
EdidString string `json:"hdmi_edid_string"`
|
||||
ActiveExtension string `json:"active_extension"`
|
||||
DisplayRotation string `json:"display_rotation"`
|
||||
DisplayMaxBrightness int `json:"display_max_brightness"`
|
||||
DisplayDimAfterSec int `json:"display_dim_after_sec"`
|
||||
DisplayOffAfterSec int `json:"display_off_after_sec"`
|
||||
|
@ -107,6 +109,8 @@ var defaultConfig = &Config{
|
|||
AutoUpdateEnabled: true, // Set a default value
|
||||
ActiveExtension: "",
|
||||
KeyboardMacros: []KeyboardMacro{},
|
||||
DisplayRotation: "270",
|
||||
KeyboardLayout: "en-US",
|
||||
DisplayMaxBrightness: 64,
|
||||
DisplayDimAfterSec: 120, // 2 minutes
|
||||
DisplayOffAfterSec: 1800, // 30 minutes
|
||||
|
|
|
@ -24,6 +24,7 @@ show_help() {
|
|||
REMOTE_USER="root"
|
||||
REMOTE_PATH="/userdata/jetkvm/bin"
|
||||
SKIP_UI_BUILD=false
|
||||
RESET_USB_HID_DEVICE=false
|
||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||
|
||||
# Parse command line arguments
|
||||
|
@ -41,6 +42,10 @@ while [[ $# -gt 0 ]]; do
|
|||
SKIP_UI_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--reset-usb-hid)
|
||||
RESET_USB_HID_DEVICE=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
|
@ -74,6 +79,12 @@ ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
|||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
set -e
|
||||
|
|
|
@ -73,6 +73,10 @@ func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) {
|
|||
return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src})
|
||||
}
|
||||
|
||||
func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation})
|
||||
}
|
||||
|
||||
func updateLabelIfChanged(objName string, newText string) {
|
||||
if newText != "" && newText != displayedTexts[objName] {
|
||||
_, _ = lvLabelSetText(objName, newText)
|
||||
|
@ -373,6 +377,7 @@ func init() {
|
|||
waitCtrlClientConnected()
|
||||
displayLogger.Info().Msg("setting initial display contents")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = lvDispSetRotation(config.DisplayRotation)
|
||||
updateStaticContents()
|
||||
displayInited = true
|
||||
displayLogger.Info().Msg("display inited")
|
||||
|
|
|
@ -13,7 +13,8 @@ var defaultNTPServers = []string{
|
|||
"time.aws.com",
|
||||
"time.windows.com",
|
||||
"time.google.com",
|
||||
"162.159.200.123", // time.cloudflare.com
|
||||
"162.159.200.123", // time.cloudflare.com IPv4
|
||||
"2606:4700:f1::123", // time.cloudflare.com IPv6
|
||||
"0.pool.ntp.org",
|
||||
"1.pool.ntp.org",
|
||||
"2.pool.ntp.org",
|
||||
|
@ -57,6 +58,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
|
||||
// query the server
|
||||
now, response, err := queryNtpServer(server, timeout)
|
||||
if err != nil {
|
||||
scopedLogger.Warn().
|
||||
Str("error", err.Error()).
|
||||
Msg("failed to query NTP server")
|
||||
results <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// set the last RTT
|
||||
metricNtpServerLastRTT.WithLabelValues(
|
||||
|
@ -76,32 +84,33 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
strconv.Itoa(int(response.Precision)),
|
||||
).Set(1)
|
||||
|
||||
if err == nil {
|
||||
// increase success count
|
||||
metricNtpTotalSuccessCount.Inc()
|
||||
metricNtpSuccessCount.WithLabelValues(server).Inc()
|
||||
// increase success count
|
||||
metricNtpTotalSuccessCount.Inc()
|
||||
metricNtpSuccessCount.WithLabelValues(server).Inc()
|
||||
|
||||
scopedLogger.Info().
|
||||
Str("time", now.Format(time.RFC3339)).
|
||||
Str("reference", response.ReferenceString()).
|
||||
Str("rtt", response.RTT.String()).
|
||||
Str("clockOffset", response.ClockOffset.String()).
|
||||
Uint8("stratum", response.Stratum).
|
||||
Msg("NTP server returned time")
|
||||
results <- &ntpResult{
|
||||
now: now,
|
||||
offset: &response.ClockOffset,
|
||||
}
|
||||
} else {
|
||||
scopedLogger.Warn().
|
||||
Str("error", err.Error()).
|
||||
Msg("failed to query NTP server")
|
||||
scopedLogger.Info().
|
||||
Str("time", now.Format(time.RFC3339)).
|
||||
Str("reference", response.ReferenceString()).
|
||||
Str("rtt", response.RTT.String()).
|
||||
Str("clockOffset", response.ClockOffset.String()).
|
||||
Uint8("stratum", response.Stratum).
|
||||
Msg("NTP server returned time")
|
||||
results <- &ntpResult{
|
||||
now: now,
|
||||
offset: &response.ClockOffset,
|
||||
}
|
||||
}(server)
|
||||
}
|
||||
|
||||
result := <-results
|
||||
return result.now, result.offset
|
||||
for range servers {
|
||||
result := <-results
|
||||
if result == nil {
|
||||
continue
|
||||
}
|
||||
now, offset = result.now, result.offset
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func queryNtpServer(server string, timeout time.Duration) (now *time.Time, response *ntp.Response, err error) {
|
||||
|
|
|
@ -137,6 +137,29 @@ func (u *UsbGadget) GetPath(itemKey string) (string, error) {
|
|||
return joinPath(u.kvmGadgetPath, item.path), nil
|
||||
}
|
||||
|
||||
// OverrideGadgetConfig overrides the gadget config for the given item and attribute.
|
||||
// It returns an error if the item is not found or the attribute is not found.
|
||||
// It returns true if the attribute is overridden, false otherwise.
|
||||
func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) {
|
||||
u.configLock.Lock()
|
||||
defer u.configLock.Unlock()
|
||||
|
||||
// get it as a pointer
|
||||
_, ok := u.configMap[itemKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("config item %s not found", itemKey), false
|
||||
}
|
||||
|
||||
if u.configMap[itemKey].attrs[itemAttr] == value {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
u.configMap[itemKey].attrs[itemAttr] = value
|
||||
u.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("overriding gadget config")
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func mountConfigFS() error {
|
||||
_, err := os.Stat(gadgetPath)
|
||||
// TODO: check if it's mounted properly
|
||||
|
|
|
@ -74,7 +74,7 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
|
||||
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8, hold bool) error {
|
||||
u.keyboardLock.Lock()
|
||||
defer u.keyboardLock.Unlock()
|
||||
|
||||
|
@ -90,6 +90,13 @@ func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !hold {
|
||||
err := u.keyboardWriteHidFile(make([]uint8, 8))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
u.resetUserInputTime()
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -55,6 +55,8 @@ var absoluteMouseCombinedReportDesc = []byte{
|
|||
0x09, 0x38, // Usage (Wheel)
|
||||
0x15, 0x81, // Logical Minimum (-127)
|
||||
0x25, 0x7F, // Logical Maximum (127)
|
||||
0x35, 0x00, // Physical Minimum (0) = Reset Physical Minimum
|
||||
0x45, 0x00, // Physical Maximum (0) = Reset Physical Maximum
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x06, // Input (Data, Var, Rel)
|
||||
|
|
|
@ -14,10 +14,13 @@ var massStorageLun0Config = gadgetConfigItem{
|
|||
order: 3001,
|
||||
path: []string{"functions", "mass_storage.usb0", "lun.0"},
|
||||
attrs: gadgetAttributes{
|
||||
"cdrom": "1",
|
||||
"ro": "1",
|
||||
"removable": "1",
|
||||
"file": "\n",
|
||||
"inquiry_string": "JetKVM Virtual Media",
|
||||
"cdrom": "1",
|
||||
"ro": "1",
|
||||
"removable": "1",
|
||||
"file": "\n",
|
||||
// the additional whitespace is intentional to avoid the "JetKVM V irtual Media" string
|
||||
// https://github.com/jetkvm/rv1106-system/blob/778133a1c153041e73f7de86c9c434a2753ea65d/sysdrv/source/uboot/u-boot/drivers/usb/gadget/f_mass_storage.c#L2556
|
||||
// Vendor (8 chars), product (16 chars)
|
||||
"inquiry_string": "JetKVM Virtual Media",
|
||||
},
|
||||
}
|
||||
|
|
49
jsonrpc.go
49
jsonrpc.go
|
@ -38,6 +38,10 @@ type JSONRPCEvent struct {
|
|||
Params interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type DisplayRotationSettings struct {
|
||||
Rotation string `json:"rotation"`
|
||||
}
|
||||
|
||||
type BacklightSettings struct {
|
||||
MaxBrightness int `json:"max_brightness"`
|
||||
DimAfter int `json:"dim_after"`
|
||||
|
@ -280,6 +284,24 @@ func rpcTryUpdate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func rpcSetDisplayRotation(params DisplayRotationSettings) error {
|
||||
var err error
|
||||
_, err = lvDispSetRotation(params.Rotation)
|
||||
if err == nil {
|
||||
config.DisplayRotation = params.Rotation
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func rpcGetDisplayRotation() (*DisplayRotationSettings, error) {
|
||||
return &DisplayRotationSettings{
|
||||
Rotation: config.DisplayRotation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rpcSetBacklightSettings(params BacklightSettings) error {
|
||||
blConfig := params
|
||||
|
||||
|
@ -544,9 +566,12 @@ type RPCHandler struct {
|
|||
func rpcSetMassStorageMode(mode string) (string, error) {
|
||||
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
||||
var cdrom bool
|
||||
if mode == "cdrom" {
|
||||
switch mode {
|
||||
case "cdrom":
|
||||
cdrom = true
|
||||
} else if mode != "file" {
|
||||
case "file":
|
||||
cdrom = false
|
||||
default:
|
||||
logger.Info().Str("mode", mode).Msg("Invalid mode provided")
|
||||
return "", fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
|
@ -565,7 +590,7 @@ func rpcSetMassStorageMode(mode string) (string, error) {
|
|||
}
|
||||
|
||||
func rpcGetMassStorageMode() (string, error) {
|
||||
cdrom, err := getMassStorageMode()
|
||||
cdrom, err := getMassStorageCDROMEnabled()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get mass storage mode: %w", err)
|
||||
}
|
||||
|
@ -863,6 +888,18 @@ func rpcSetScrollSensitivity(sensitivity string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func rpcGetKeyboardLayout() (string, error) {
|
||||
return config.KeyboardLayout, nil
|
||||
}
|
||||
|
||||
func rpcSetKeyboardLayout(layout string) error {
|
||||
config.KeyboardLayout = layout
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getKeyboardMacros() (interface{}, error) {
|
||||
macros := make([]KeyboardMacro, len(config.KeyboardMacros))
|
||||
copy(macros, config.KeyboardMacros)
|
||||
|
@ -966,7 +1003,7 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getNetworkSettings": {Func: rpcGetNetworkSettings},
|
||||
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys", "hold"}},
|
||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
||||
|
@ -1012,6 +1049,8 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
||||
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
||||
"resetConfig": {Func: rpcResetConfig},
|
||||
"setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}},
|
||||
"getDisplayRotation": {Func: rpcGetDisplayRotation},
|
||||
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
||||
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
||||
"getDCPowerState": {Func: rpcGetDCPowerState},
|
||||
|
@ -1028,6 +1067,8 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
||||
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
|
||||
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
|
||||
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
|
||||
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
||||
"getKeyboardMacros": {Func: getKeyboardMacros},
|
||||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||
}
|
||||
|
|
8
main.go
8
main.go
|
@ -77,6 +77,14 @@ func Main() {
|
|||
|
||||
initUsbGadget()
|
||||
|
||||
if err := setInitialVirtualMediaState(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
|
||||
}
|
||||
|
||||
if err := initImagesFolder(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to init images folder")
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(15 * time.Minute)
|
||||
for {
|
||||
|
|
Binary file not shown.
|
@ -1 +1 @@
|
|||
4b925c7aa73d2e35a227833e806658cb17e1d25900611f93ed70b11ac9f1716d
|
||||
6dabd0e657dd099280d9173069687786a4a8c9c25cf7f9e7ce2f940cab67c521
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:import/recommended",
|
||||
"prettier",
|
||||
],
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["react-refresh"],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
* This keeps imports separate from one another, ensuring that imports are separated
|
||||
* by their relative groups. As you move through the groups, imports become closer
|
||||
* to the current file.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* import fs from 'fs';
|
||||
*
|
||||
* import package from 'npm-package';
|
||||
*
|
||||
* import xyz from '~/project-file';
|
||||
*
|
||||
* import index from '../';
|
||||
*
|
||||
* import sibling from './foo';
|
||||
* ```
|
||||
*/
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||
"newlines-between": "always",
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
alias: {
|
||||
map: [
|
||||
["@components", "./src/components"],
|
||||
["@routes", "./src/routes"],
|
||||
["@assets", "./src/assets"],
|
||||
["@", "./src"],
|
||||
],
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
const {
|
||||
defineConfig,
|
||||
globalIgnores,
|
||||
} = require("eslint/config");
|
||||
|
||||
const globals = require("globals");
|
||||
|
||||
const {
|
||||
fixupConfigRules,
|
||||
} = require("@eslint/compat");
|
||||
|
||||
const tsParser = require("@typescript-eslint/parser");
|
||||
const reactRefresh = require("eslint-plugin-react-refresh");
|
||||
const js = require("@eslint/js");
|
||||
|
||||
const {
|
||||
FlatCompat,
|
||||
} = require("@eslint/eslintrc");
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
module.exports = defineConfig([{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
extends: fixupConfigRules(compat.extends(
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:import/recommended",
|
||||
"prettier",
|
||||
)),
|
||||
|
||||
plugins: {
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"react-refresh/only-export-components": ["warn", {
|
||||
allowConstantExport: true,
|
||||
}],
|
||||
|
||||
"import/order": ["error", {
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||
"newlines-between": "always",
|
||||
}],
|
||||
},
|
||||
|
||||
settings: {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
},
|
||||
"import/resolver": {
|
||||
alias: {
|
||||
map: [
|
||||
["@components", "./src/components"],
|
||||
["@routes", "./src/routes"],
|
||||
["@assets", "./src/assets"],
|
||||
["@", "./src"],
|
||||
],
|
||||
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}, globalIgnores([
|
||||
"**/dist",
|
||||
"**/.eslintrc.cjs",
|
||||
"**/tailwind.config.js",
|
||||
"**/postcss.config.js",
|
||||
])]);
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "21.1.0"
|
||||
"node": "22.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "./dev_device.sh",
|
||||
|
@ -19,62 +19,67 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@headlessui/react": "^2.2.3",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@xterm/addon-clipboard": "^0.1.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-unicode11": "^0.8.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"cva": "^1.0.0-beta.1",
|
||||
"cva": "^1.0.0-beta.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"focus-trap-react": "^10.2.3",
|
||||
"framer-motion": "^11.15.0",
|
||||
"focus-trap-react": "^11.0.3",
|
||||
"framer-motion": "^12.11.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react": "^19.1.0",
|
||||
"react-animate-height": "^3.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-simple-keyboard": "^3.7.112",
|
||||
"react-simple-keyboard": "^3.8.72",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.9",
|
||||
"recharts": "^2.15.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"validator": "^13.12.0",
|
||||
"xterm": "^5.3.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^2.15.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"validator": "^13.15.0",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/validator": "^13.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||
"@typescript-eslint/parser": "^8.25.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.6",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/validator": "^13.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.1.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.2.0",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export default function AuthLayout({
|
|||
<>
|
||||
<GridBackground />
|
||||
|
||||
<div className="grid min-h-screen grid-rows-layout">
|
||||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<SimpleNavbar
|
||||
logoHref="/"
|
||||
actionElement={
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { JSX } from "react";
|
||||
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
||||
|
||||
import ExtLink from "@/components/ExtLink";
|
||||
|
@ -16,7 +16,7 @@ const sizes = {
|
|||
const themes = {
|
||||
primary: cx(
|
||||
// Base styles
|
||||
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow",
|
||||
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow-sm",
|
||||
// Hover states
|
||||
"group-hover:bg-blue-800",
|
||||
// Active states
|
||||
|
@ -24,7 +24,7 @@ const themes = {
|
|||
),
|
||||
danger: cx(
|
||||
// Base styles
|
||||
"bg-red-600 text-white border-red-700 shadow-sm shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
|
||||
"bg-red-600 text-white border-red-700 shadow-xs shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
|
||||
// Hover states
|
||||
"group-hover:bg-red-700 group-hover:border-red-800 dark:group-hover:bg-red-700 dark:group-hover:border-red-600",
|
||||
// Active states
|
||||
|
@ -34,7 +34,7 @@ const themes = {
|
|||
),
|
||||
light: cx(
|
||||
// Base styles
|
||||
"bg-white text-black border-slate-800/30 shadow dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
|
||||
"bg-white text-black border-slate-800/30 shadow-xs dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
|
||||
// Hover states
|
||||
"group-hover:bg-blue-50/80 dark:group-hover:bg-slate-700",
|
||||
// Active states
|
||||
|
@ -44,7 +44,7 @@ const themes = {
|
|||
),
|
||||
lightDanger: cx(
|
||||
// Base styles
|
||||
"bg-white text-black border-red-400/60 shadow-sm",
|
||||
"bg-white text-black border-red-400/60 shadow-xs",
|
||||
// Hover states
|
||||
"group-hover:bg-red-50/80",
|
||||
// Active states
|
||||
|
@ -56,7 +56,7 @@ const themes = {
|
|||
// Base styles
|
||||
"bg-white/0 text-black border-transparent dark:text-white",
|
||||
// Hover states
|
||||
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
|
||||
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow-sm dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
|
||||
// Active states
|
||||
"group-active:bg-slate-100/80",
|
||||
),
|
||||
|
@ -65,15 +65,15 @@ const themes = {
|
|||
const btnVariants = cva({
|
||||
base: cx(
|
||||
// Base styles
|
||||
"border rounded select-none",
|
||||
"border rounded-sm select-none",
|
||||
// Size classes
|
||||
"justify-center items-center shrink-0",
|
||||
// Transition classes
|
||||
"outline-none transition-all duration-200",
|
||||
"outline-hidden transition-all duration-200",
|
||||
// Text classes
|
||||
"font-display text-center font-medium leading-tight",
|
||||
// States
|
||||
"group-focus:outline-none group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
|
||||
"group-focus:outline-hidden group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
|
||||
"group-disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
),
|
||||
|
||||
|
@ -175,7 +175,7 @@ type ButtonPropsType = Pick<
|
|||
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
|
||||
const classes = cx(
|
||||
"group outline-none",
|
||||
"group outline-hidden",
|
||||
props.fullWidth ? "w-full" : "",
|
||||
loading ? "pointer-events-none" : "",
|
||||
);
|
||||
|
@ -215,7 +215,7 @@ type LinkPropsType = Pick<LinkProps, "to"> &
|
|||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
const classes = cx(
|
||||
"group outline-none",
|
||||
"group outline-hidden",
|
||||
props.disabled ? "pointer-events-none !opacity-70" : "",
|
||||
props.fullWidth ? "w-full" : "",
|
||||
props.loading ? "pointer-events-none" : "",
|
||||
|
@ -241,7 +241,7 @@ type LabelPropsType = Pick<HTMLLabelElement, "htmlFor"> &
|
|||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||
export const LabelButton = ({ htmlFor, ...props }: LabelPropsType) => {
|
||||
const classes = cx(
|
||||
"group outline-none block cursor-pointer",
|
||||
"group outline-hidden block cursor-pointer",
|
||||
props.disabled ? "pointer-events-none !opacity-70" : "",
|
||||
props.fullWidth ? "w-full" : "",
|
||||
props.loading ? "pointer-events-none" : "",
|
||||
|
|
|
@ -30,7 +30,7 @@ const Card = forwardRef<HTMLDivElement, CardPropsType>(({ children, className },
|
|||
<div
|
||||
ref={ref}
|
||||
className={cx(
|
||||
"w-full rounded border-none bg-white shadow outline outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20",
|
||||
"w-full rounded-sm border-none bg-white shadow-xs outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Ref } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
@ -15,7 +15,7 @@ const checkboxVariants = cva({
|
|||
"block rounded",
|
||||
|
||||
// Colors
|
||||
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 text-blue-700 dark:text-blue-500 transition-colors",
|
||||
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors",
|
||||
|
||||
// Hover
|
||||
"hover:bg-slate-200/50 dark:hover:bg-slate-700/50",
|
||||
|
@ -24,7 +24,7 @@ const checkboxVariants = cva({
|
|||
"active:bg-slate-200 dark:active:bg-slate-700",
|
||||
|
||||
// Focus
|
||||
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
|
||||
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-hidden focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
|
||||
|
||||
// Disabled
|
||||
"disabled:pointer-events-none disabled:opacity-30",
|
||||
|
@ -41,7 +41,9 @@ const Checkbox = forwardRef<HTMLInputElement, CheckBoxProps>(function Checkbox(
|
|||
ref,
|
||||
) {
|
||||
const classes = checkboxVariants({ size });
|
||||
return <input ref={ref} {...props} type="checkbox" className={clsx(classes, className)} />;
|
||||
return (
|
||||
<input ref={ref} {...props} type="checkbox" className={clsx(classes, className)} />
|
||||
);
|
||||
});
|
||||
Checkbox.displayName = "Checkbox";
|
||||
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import { useRef } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Combobox as HeadlessCombobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react";
|
||||
import {
|
||||
Combobox as HeadlessCombobox,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/react";
|
||||
|
||||
import { cva } from "@/cva.config";
|
||||
|
||||
import Card from "./Card";
|
||||
|
||||
export interface ComboboxOption {
|
||||
|
@ -22,7 +29,7 @@ const comboboxVariants = cva({
|
|||
|
||||
type BaseProps = React.ComponentProps<typeof HeadlessCombobox>;
|
||||
|
||||
interface ComboboxProps extends Omit<BaseProps, 'displayValue'> {
|
||||
interface ComboboxProps extends Omit<BaseProps, "displayValue"> {
|
||||
displayValue: (option: ComboboxOption) => string;
|
||||
onInputChange: (option: string) => void;
|
||||
options: () => ComboboxOption[];
|
||||
|
@ -48,72 +55,68 @@ export function Combobox({
|
|||
const classes = comboboxVariants({ size });
|
||||
|
||||
return (
|
||||
<HeadlessCombobox
|
||||
onChange={onChange}
|
||||
{...otherProps}
|
||||
>
|
||||
<HeadlessCombobox onChange={onChange} {...otherProps}>
|
||||
{() => (
|
||||
<>
|
||||
<Card className="w-auto !border border-solid !border-slate-800/30 shadow outline-0 dark:!border-slate-300/30">
|
||||
<Card className="w-auto !border border-solid !border-slate-800/30 shadow-xs outline-0 dark:!border-slate-300/30">
|
||||
<ComboboxInput
|
||||
ref={inputRef}
|
||||
className={clsx(
|
||||
classes,
|
||||
|
||||
// General styling
|
||||
"block w-full cursor-pointer rounded border-none py-0 font-medium shadow-none outline-0 transition duration-300",
|
||||
|
||||
// Hover
|
||||
"hover:bg-blue-50/80 active:bg-blue-100/60",
|
||||
|
||||
// Dark mode
|
||||
"dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700 dark:active:bg-slate-800/60",
|
||||
|
||||
// Focus
|
||||
"focus:outline-blue-600 focus:ring-2 focus:ring-blue-700 focus:ring-offset-2 dark:focus:outline-blue-500 dark:focus:ring-blue-500",
|
||||
|
||||
// Disabled
|
||||
disabled && "pointer-events-none select-none bg-slate-50 text-slate-500/80 dark:bg-slate-800 dark:text-slate-400/80 disabled:hover:bg-white dark:disabled:hover:bg-slate-800"
|
||||
)}
|
||||
placeholder={disabled ? disabledMessage : placeholder}
|
||||
displayValue={displayValue}
|
||||
onChange={(event) => onInputChange(event.target.value)}
|
||||
disabled={disabled}
|
||||
ref={inputRef}
|
||||
className={clsx(
|
||||
classes,
|
||||
|
||||
// General styling
|
||||
"block w-full cursor-pointer rounded border-none py-0 font-medium shadow-none outline-0 transition duration-300",
|
||||
|
||||
// Hover
|
||||
"hover:bg-blue-50/80 active:bg-blue-100/60",
|
||||
|
||||
// Dark mode
|
||||
"dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700 dark:active:bg-slate-800/60",
|
||||
|
||||
// Focus
|
||||
"focus:outline-blue-600 focus:ring-2 focus:ring-blue-700 focus:ring-offset-2 dark:focus:outline-blue-500 dark:focus:ring-blue-500",
|
||||
|
||||
// Disabled
|
||||
disabled &&
|
||||
"pointer-events-none select-none bg-slate-50 text-slate-500/80 disabled:hover:bg-white dark:bg-slate-800 dark:text-slate-400/80 dark:disabled:hover:bg-slate-800",
|
||||
)}
|
||||
placeholder={disabled ? disabledMessage : placeholder}
|
||||
displayValue={displayValue}
|
||||
onChange={event => onInputChange(event.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
|
||||
{options().length > 0 && (
|
||||
<ComboboxOptions className="absolute left-0 z-[100] mt-1 w-full max-h-60 overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700 hide-scrollbar">
|
||||
{options().map((option) => (
|
||||
<ComboboxOption
|
||||
key={option.value}
|
||||
value={option}
|
||||
className={clsx(
|
||||
// General styling
|
||||
"cursor-default select-none py-2 px-4",
|
||||
|
||||
// Hover and active states
|
||||
"hover:bg-blue-50/80 ui-active:bg-blue-50/80 ui-active:text-blue-900",
|
||||
|
||||
// Dark mode
|
||||
"dark:text-slate-300 dark:hover:bg-slate-700 dark:ui-active:bg-slate-700 dark:ui-active:text-blue-200"
|
||||
)}
|
||||
<ComboboxOptions className="hide-scrollbar absolute left-0 z-[100] mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700">
|
||||
{options().map(option => (
|
||||
<ComboboxOption
|
||||
key={option.value}
|
||||
value={option}
|
||||
className={clsx(
|
||||
// General styling
|
||||
"cursor-default select-none px-4 py-2",
|
||||
|
||||
// Hover and active states
|
||||
"hover:bg-blue-50/80 ui-active:bg-blue-50/80 ui-active:text-blue-900",
|
||||
|
||||
// Dark mode
|
||||
"dark:text-slate-300 dark:hover:bg-slate-700 dark:ui-active:bg-slate-700 dark:ui-active:text-blue-200",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</ComboboxOption>
|
||||
))}
|
||||
</ComboboxOptions>
|
||||
)}
|
||||
|
||||
|
||||
{options().length === 0 && inputRef.current?.value && (
|
||||
<div className="absolute left-0 z-[100] mt-1 w-full rounded-md bg-white dark:bg-slate-800 py-2 px-4 text-sm shadow-lg ring-1 ring-black/5 dark:ring-slate-700">
|
||||
<div className="text-slate-500 dark:text-slate-400">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
<div className="absolute left-0 z-[100] mt-1 w-full rounded-md bg-white px-4 py-2 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700">
|
||||
<div className="text-slate-500 dark:text-slate-400">{emptyMessage}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HeadlessCombobox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import { Button } from "@/components/Button";
|
||||
import Modal from "@/components/Modal";
|
||||
|
@ -42,12 +47,15 @@ const variantConfig = {
|
|||
iconBgClass: "bg-blue-100",
|
||||
buttonTheme: "primary",
|
||||
},
|
||||
} as Record<Variant, {
|
||||
} as Record<
|
||||
Variant,
|
||||
{
|
||||
icon: React.ElementType;
|
||||
iconClass: string;
|
||||
iconBgClass: string;
|
||||
buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger";
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
|
@ -65,13 +73,18 @@ export function ConfirmDialog({
|
|||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<div className="mx-auto max-w-xl px-4 transition-all duration-300 ease-in-out">
|
||||
<div className="relative w-full overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-800 pointer-events-auto">
|
||||
<div className="pointer-events-auto relative w-full overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-800">
|
||||
<div className="space-y-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className={cx("mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10", iconBgClass)}>
|
||||
<div
|
||||
className={cx(
|
||||
"mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
|
||||
iconBgClass,
|
||||
)}
|
||||
>
|
||||
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h2 className="text-lg font-bold leading-tight text-black dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
|
@ -83,12 +96,7 @@ export function ConfirmDialog({
|
|||
|
||||
<div className="flex justify-end gap-x-2">
|
||||
{cancelText && (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text={cancelText}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
|
||||
)}
|
||||
<Button
|
||||
size="SM"
|
||||
|
@ -103,4 +111,4 @@ export function ConfirmDialog({
|
|||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ export default function EmptyCard({
|
|||
<div className="max-w-[90%] space-y-1.5 text-center md:max-w-[60%]">
|
||||
<div className="space-y-2">
|
||||
{IconElm && (
|
||||
<IconElm className="mx-auto h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
<IconElm className="mx-auto h-5 w-5 text-blue-600 dark:text-blue-600" />
|
||||
)}
|
||||
<h4 className="text-base font-bold leading-none text-black dark:text-white">
|
||||
{headline}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export default function GridBackground() {
|
||||
return (
|
||||
<div className="absolute w-screen h-screen overflow-hidden isolate opacity-60">
|
||||
<div className="absolute isolate h-screen w-screen overflow-hidden opacity-60">
|
||||
<svg
|
||||
className="absolute inset-x-0 top-0 -z-10 h-[64rem] w-full stroke-gray-300 [mask-image:radial-gradient(32rem_32rem_at_center,white,transparent)] dark:stroke-slate-300/20"
|
||||
className="absolute inset-x-0 top-0 -z-10 h-full w-full mask-radial-[32rem_32rem] mask-radial-from-white mask-radial-to-transparent mask-radial-at-center stroke-gray-300 dark:stroke-slate-300/20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Fragment, useCallback } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||
import { Menu, MenuButton } from "@headlessui/react";
|
||||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||
|
||||
import Container from "@/components/Container";
|
||||
import Card from "@/components/Card";
|
||||
import { cx } from "@/cva.config";
|
||||
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
|
@ -17,7 +16,7 @@ import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
|||
import api from "../api";
|
||||
import { isOnDevice } from "../main";
|
||||
|
||||
import { Button, LinkButton } from "./Button";
|
||||
import { LinkButton } from "./Button";
|
||||
|
||||
interface NavbarProps {
|
||||
isLoggedIn: boolean;
|
||||
|
@ -51,8 +50,12 @@ export default function DashboardNavbar({
|
|||
|
||||
const usbState = useHidStore(state => state.usbState);
|
||||
|
||||
// for testing
|
||||
//userEmail = "user@example.org";
|
||||
//picture = "https://placehold.co/32x32"
|
||||
|
||||
return (
|
||||
<div className="w-full select-none border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
|
||||
<div className="w-full border-b border-b-slate-800/20 bg-white select-none dark:border-b-slate-300/20 dark:bg-slate-900">
|
||||
<Container>
|
||||
<div className="flex h-14 items-center justify-between">
|
||||
<div className="flex shrink-0 items-center gap-x-8">
|
||||
|
@ -78,86 +81,82 @@ export default function DashboardNavbar({
|
|||
</div>
|
||||
<div className="flex w-full items-center justify-end gap-x-2">
|
||||
<div className="flex shrink-0 items-center space-x-4">
|
||||
{showConnectionStatus && (
|
||||
<div className="hidden items-center gap-x-2 md:flex">
|
||||
<div className="w-[159px]">
|
||||
<PeerConnectionStatusCard
|
||||
state={peerConnectionState}
|
||||
title={kvmName}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden w-[159px] md:block">
|
||||
<USBStateStatus
|
||||
state={usbState}
|
||||
<div className="hidden items-stretch gap-x-2 md:flex">
|
||||
{showConnectionStatus && (
|
||||
<>
|
||||
<div className="w-[159px]">
|
||||
<PeerConnectionStatusCard
|
||||
state={peerConnectionState}
|
||||
title={kvmName}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden w-[159px] md:block">
|
||||
<USBStateStatus
|
||||
state={usbState}
|
||||
peerConnectionState={peerConnectionState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<hr className="h-[20px] w-[1px] border-none bg-slate-800/20 dark:bg-slate-300/20" />
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<MenuButton as={Fragment}>
|
||||
<Button
|
||||
theme="blank"
|
||||
size="SM"
|
||||
text={
|
||||
<>
|
||||
{picture ? <></> : userEmail}
|
||||
<ChevronDownIcon className="h-4 w-4 shrink-0 text-slate-900 dark:text-white" />
|
||||
</>
|
||||
}
|
||||
LeadingIcon={({ className }) =>
|
||||
picture && (
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<hr className="h-[20px] w-[1px] self-center border-none bg-slate-800/20 dark:bg-slate-300/20" />
|
||||
<div className="relative inline-block text-left">
|
||||
<Menu>
|
||||
<MenuButton className="h-full">
|
||||
<Button className="flex h-full items-center gap-x-3 rounded-md border border-slate-800/20 bg-white px-2 py-1.5 dark:border-slate-600 dark:bg-slate-800 dark:text-white">
|
||||
{picture ? (
|
||||
<img
|
||||
src={picture}
|
||||
alt="Avatar"
|
||||
className={cx(
|
||||
className,
|
||||
"h-8 w-8 rounded-full border-2 border-transparent transition-colors group-hover:border-blue-700",
|
||||
)}
|
||||
className="size-6 rounded-full border-2 border-transparent transition-colors group-hover:border-blue-700"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right focus:outline-none">
|
||||
<Card className="overflow-hidden">
|
||||
<div className="space-y-1 p-1 dark:text-white">
|
||||
{userEmail && (
|
||||
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
|
||||
<Menu.Item>
|
||||
<div className="p-2">
|
||||
<div className="font-display text-xs">Logged in as</div>
|
||||
<div className="w-[200px] truncate font-display text-sm font-semibold">
|
||||
{userEmail}
|
||||
) : userEmail ? (
|
||||
<span className="font-display max-w-[200px] truncate text-sm/6 font-semibold">
|
||||
{userEmail}
|
||||
</span>
|
||||
) : null}
|
||||
<ChevronDownIcon className="size-4 shrink-0 text-slate-900 dark:text-white" />
|
||||
</Button>
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
transition
|
||||
anchor="bottom end"
|
||||
className="right-0 mt-1 w-56 origin-top-right p-px focus:outline-hidden data-closed:opacity-0"
|
||||
>
|
||||
<MenuItem>
|
||||
<Card className="overflow-hidden">
|
||||
{userEmail && (
|
||||
<div className="space-y-1 p-1 dark:text-white">
|
||||
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
|
||||
<div className="p-2">
|
||||
<div className="font-display text-xs">
|
||||
Logged in as
|
||||
</div>
|
||||
<div className="font-display max-w-[200px] truncate text-sm font-semibold">
|
||||
{userEmail}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Menu.Item>
|
||||
<div onClick={onLogout}>
|
||||
<button className="block w-full">
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<ArrowLeftEndOnRectangleIcon className="h-4 w-4" />
|
||||
<div className="font-display">Log out</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="space-y-1 p-1 dark:text-white"
|
||||
onClick={onLogout}
|
||||
>
|
||||
<button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<ArrowLeftEndOnRectangleIcon className="size-4" />
|
||||
<div className="font-display">Log out</div>
|
||||
</button>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</>
|
||||
) : null}
|
||||
</Card>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Ref } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
@ -44,7 +44,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
|||
"[&:has(:user-invalid)]:ring-2 [&:has(:user-invalid)]:ring-red-600 [&:has(:user-invalid)]:ring-offset-2",
|
||||
|
||||
// Focus Within
|
||||
"focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2",
|
||||
"focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-hidden focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2",
|
||||
|
||||
// Disabled Within
|
||||
"disabled-within:pointer-events-none disabled-within:select-none disabled-within:bg-slate-50 dark:disabled-within:bg-slate-800 disabled-within:text-slate-500/80",
|
||||
|
|
|
@ -113,7 +113,7 @@ export default function KvmCard({
|
|||
transition
|
||||
className="data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
|
||||
>
|
||||
<Card className="absolute right-0 z-10 w-56 px-1 mt-2 transition origin-top-right ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Card className="absolute right-0 z-10 w-56 px-1 mt-2 transition origin-top-right ring-1 ring-black/50 focus:outline-hidden">
|
||||
<div className="divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||
<MenuItem>
|
||||
<div>
|
||||
|
|
|
@ -7,7 +7,7 @@ export default function LoadingSpinner({
|
|||
}) {
|
||||
return (
|
||||
<svg
|
||||
className={clsx(className, "flex-shrink-0 animate-spin p-[2px]")}
|
||||
className={clsx(className, "shrink-0 animate-spin p-[2px]")}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { LuPlus } from "react-icons/lu";
|
||||
|
||||
import { KeySequence } from "@/hooks/stores";
|
||||
|
@ -7,16 +6,23 @@ import { Button } from "@/components/Button";
|
|||
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { MacroStepCard } from "@/components/MacroStepCard";
|
||||
import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP } from "@/constants/macros";
|
||||
import {
|
||||
DEFAULT_DELAY,
|
||||
MAX_STEPS_PER_MACRO,
|
||||
MAX_KEYS_PER_STEP,
|
||||
} from "@/constants/macros";
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
||||
interface ValidationErrors {
|
||||
name?: string;
|
||||
steps?: Record<number, {
|
||||
keys?: string;
|
||||
modifiers?: string;
|
||||
delay?: string;
|
||||
}>;
|
||||
steps?: Record<
|
||||
number,
|
||||
{
|
||||
keys?: string;
|
||||
modifiers?: string;
|
||||
delay?: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
interface MacroFormProps {
|
||||
|
@ -53,16 +59,18 @@ export function MacroForm({
|
|||
} else if (macro.name.trim().length > 50) {
|
||||
newErrors.name = "Name must be less than 50 characters";
|
||||
}
|
||||
|
||||
|
||||
if (!macro.steps?.length) {
|
||||
newErrors.steps = { 0: { keys: "At least one step is required" } };
|
||||
} else {
|
||||
const hasKeyOrModifier = macro.steps.some(step =>
|
||||
(step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0
|
||||
const hasKeyOrModifier = macro.steps.some(
|
||||
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
||||
);
|
||||
|
||||
if (!hasKeyOrModifier) {
|
||||
newErrors.steps = { 0: { keys: "At least one step must have keys or modifiers" } };
|
||||
newErrors.steps = {
|
||||
0: { keys: "At least one step must have keys or modifiers" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +95,10 @@ export function MacroForm({
|
|||
}
|
||||
};
|
||||
|
||||
const handleKeySelect = (stepIndex: number, option: { value: string | null; keys?: string[] }) => {
|
||||
const handleKeySelect = (
|
||||
stepIndex: number,
|
||||
option: { value: string | null; keys?: string[] },
|
||||
) => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
if (!newSteps[stepIndex]) return;
|
||||
|
||||
|
@ -97,7 +108,9 @@ export function MacroForm({
|
|||
if (!newSteps[stepIndex].keys) {
|
||||
newSteps[stepIndex].keys = [];
|
||||
}
|
||||
const keysArray = Array.isArray(newSteps[stepIndex].keys) ? newSteps[stepIndex].keys : [];
|
||||
const keysArray = Array.isArray(newSteps[stepIndex].keys)
|
||||
? newSteps[stepIndex].keys
|
||||
: [];
|
||||
if (keysArray.length >= MAX_KEYS_PER_STEP) {
|
||||
showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`);
|
||||
return;
|
||||
|
@ -105,7 +118,7 @@ export function MacroForm({
|
|||
newSteps[stepIndex].keys = [...keysArray, option.value];
|
||||
}
|
||||
setMacro({ ...macro, steps: newSteps });
|
||||
|
||||
|
||||
if (errors.steps?.[stepIndex]?.keys) {
|
||||
const newErrors = { ...errors };
|
||||
delete newErrors.steps?.[stepIndex].keys;
|
||||
|
@ -127,7 +140,7 @@ export function MacroForm({
|
|||
const newSteps = [...(macro.steps || [])];
|
||||
newSteps[stepIndex].modifiers = modifiers;
|
||||
setMacro({ ...macro, steps: newSteps });
|
||||
|
||||
|
||||
// Clear step errors when modifiers are added
|
||||
if (errors.steps?.[stepIndex]?.keys && modifiers.length > 0) {
|
||||
const newErrors = { ...errors };
|
||||
|
@ -148,9 +161,9 @@ export function MacroForm({
|
|||
setMacro({ ...macro, steps: newSteps });
|
||||
};
|
||||
|
||||
const handleStepMove = (stepIndex: number, direction: 'up' | 'down') => {
|
||||
const handleStepMove = (stepIndex: number, direction: "up" | "down") => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
const newIndex = direction === 'up' ? stepIndex - 1 : stepIndex + 1;
|
||||
const newIndex = direction === "up" ? stepIndex - 1 : stepIndex + 1;
|
||||
[newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]];
|
||||
setMacro({ ...macro, steps: newSteps });
|
||||
};
|
||||
|
@ -181,7 +194,10 @@ export function MacroForm({
|
|||
<div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<FieldLabel label="Steps" description={`Keys/modifiers executed in sequence with a delay between each step.`} />
|
||||
<FieldLabel
|
||||
label="Steps"
|
||||
description={`Keys/modifiers executed in sequence with a delay between each step.`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-slate-500 dark:text-slate-400">
|
||||
{macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps
|
||||
|
@ -199,18 +215,24 @@ export function MacroForm({
|
|||
key={stepIndex}
|
||||
step={step}
|
||||
stepIndex={stepIndex}
|
||||
onDelete={macro.steps && macro.steps.length > 1 ? () => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
newSteps.splice(stepIndex, 1);
|
||||
setMacro(prev => ({ ...prev, steps: newSteps }));
|
||||
} : undefined}
|
||||
onMoveUp={() => handleStepMove(stepIndex, 'up')}
|
||||
onMoveDown={() => handleStepMove(stepIndex, 'down')}
|
||||
onKeySelect={(option) => handleKeySelect(stepIndex, option)}
|
||||
onKeyQueryChange={(query) => handleKeyQueryChange(stepIndex, query)}
|
||||
keyQuery={keyQueries[stepIndex] || ''}
|
||||
onModifierChange={(modifiers) => handleModifierChange(stepIndex, modifiers)}
|
||||
onDelayChange={(delay) => handleDelayChange(stepIndex, delay)}
|
||||
onDelete={
|
||||
macro.steps && macro.steps.length > 1
|
||||
? () => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
newSteps.splice(stepIndex, 1);
|
||||
setMacro(prev => ({ ...prev, steps: newSteps }));
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onMoveUp={() => handleStepMove(stepIndex, "up")}
|
||||
onMoveDown={() => handleStepMove(stepIndex, "down")}
|
||||
onKeySelect={option => handleKeySelect(stepIndex, option)}
|
||||
onKeyQueryChange={query => handleKeyQueryChange(stepIndex, query)}
|
||||
keyQuery={keyQueries[stepIndex] || ""}
|
||||
onModifierChange={modifiers =>
|
||||
handleModifierChange(stepIndex, modifiers)
|
||||
}
|
||||
onDelayChange={delay => handleDelayChange(stepIndex, delay)}
|
||||
isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
|
||||
/>
|
||||
))}
|
||||
|
@ -223,18 +245,20 @@ export function MacroForm({
|
|||
theme="light"
|
||||
fullWidth
|
||||
LeadingIcon={LuPlus}
|
||||
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ''}`}
|
||||
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ""}`}
|
||||
onClick={() => {
|
||||
if (isMaxStepsReached) {
|
||||
showTemporaryError(`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`);
|
||||
showTemporaryError(
|
||||
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setMacro(prev => ({
|
||||
...prev,
|
||||
steps: [
|
||||
...(prev.steps || []),
|
||||
{ keys: [], modifiers: [], delay: DEFAULT_DELAY }
|
||||
...(prev.steps || []),
|
||||
{ keys: [], modifiers: [], delay: DEFAULT_DELAY },
|
||||
],
|
||||
}));
|
||||
setErrors({});
|
||||
|
@ -257,15 +281,10 @@ export function MacroForm({
|
|||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Cancel"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
@ -63,7 +63,7 @@ export const SelectMenuBasic = React.forwardRef<HTMLSelectElement, SelectMenuPro
|
|||
)}
|
||||
>
|
||||
{label && <FieldLabel label={label} id={id} as="span" />}
|
||||
<Card className="w-auto !border border-solid !border-slate-800/30 shadow outline-0 dark:!border-slate-300/30">
|
||||
<Card className="w-auto !border border-solid !border-slate-800/30 shadow-xs outline-0 dark:!border-slate-300/30">
|
||||
<select
|
||||
ref={ref}
|
||||
name={name}
|
||||
|
@ -72,7 +72,7 @@ export const SelectMenuBasic = React.forwardRef<HTMLSelectElement, SelectMenuPro
|
|||
classes,
|
||||
|
||||
// General styling
|
||||
"block w-full cursor-pointer rounded border-none py-0 font-medium shadow-none outline-0 transition duration-300",
|
||||
"block w-full cursor-pointer rounded-sm border-none py-0 font-medium shadow-none outline-0 transition duration-300",
|
||||
|
||||
// Hover
|
||||
"hover:bg-blue-50/80 active:bg-blue-100/60 disabled:hover:bg-white",
|
||||
|
|
|
@ -44,7 +44,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
|
|||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"rounded-md border border-blue-800 bg-blue-700 px-2 py-1 font-medium text-white shadow-sm dark:border-blue-300",
|
||||
"rounded-md border border-blue-800 bg-blue-700 px-2 py-1 font-medium text-white shadow-xs dark:border-blue-300",
|
||||
textStyle,
|
||||
)}
|
||||
key={`${i}-${currStepIdx}`}
|
||||
|
|
|
@ -79,10 +79,11 @@ function Terminal({
|
|||
return () => {
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
};
|
||||
}, [enableTerminal, instance, ref, setDisableKeyboardFocusTrap, type]);
|
||||
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
|
||||
|
||||
const readyState = dataChannel.readyState;
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
if (readyState !== "open") return;
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
@ -93,11 +94,10 @@ function Terminal({
|
|||
// Handle binary data differently based on browser implementation
|
||||
// Firefox sends data as blobs, chrome sends data as arraybuffer
|
||||
if (binaryType === "arraybuffer") {
|
||||
instance?.write(new Uint8Array(e.data));
|
||||
instance.write(new Uint8Array(e.data));
|
||||
} else if (binaryType === "blob") {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (!instance) return;
|
||||
if (!reader.result) return;
|
||||
instance.write(new Uint8Array(reader.result as ArrayBuffer));
|
||||
};
|
||||
|
@ -107,12 +107,12 @@ function Terminal({
|
|||
{ signal: abortController.signal },
|
||||
);
|
||||
|
||||
const onDataHandler = instance?.onData(data => {
|
||||
const onDataHandler = instance.onData(data => {
|
||||
dataChannel.send(data);
|
||||
});
|
||||
|
||||
// Setup escape key handler
|
||||
const onKeyHandler = instance?.onKey(e => {
|
||||
const onKeyHandler = instance.onKey(e => {
|
||||
const { domEvent } = e;
|
||||
if (domEvent.key === "Escape") {
|
||||
setTerminalType("none");
|
||||
|
@ -123,32 +123,32 @@ function Terminal({
|
|||
|
||||
// Send initial terminal size
|
||||
if (dataChannel.readyState === "open") {
|
||||
dataChannel.send(JSON.stringify({ rows: instance?.rows, cols: instance?.cols }));
|
||||
dataChannel.send(JSON.stringify({ rows: instance.rows, cols: instance.cols }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
onDataHandler?.dispose();
|
||||
onKeyHandler?.dispose();
|
||||
onDataHandler.dispose();
|
||||
onKeyHandler.dispose();
|
||||
};
|
||||
}, [dataChannel, instance, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
|
||||
// Load the fit addon
|
||||
const fitAddon = new FitAddon();
|
||||
instance?.loadAddon(fitAddon);
|
||||
instance.loadAddon(fitAddon);
|
||||
|
||||
instance?.loadAddon(new ClipboardAddon());
|
||||
instance?.loadAddon(new Unicode11Addon());
|
||||
instance?.loadAddon(new WebLinksAddon());
|
||||
instance.loadAddon(new ClipboardAddon());
|
||||
instance.loadAddon(new Unicode11Addon());
|
||||
instance.loadAddon(new WebLinksAddon());
|
||||
instance.unicode.activeVersion = "11";
|
||||
|
||||
if (isWebGl2Supported) {
|
||||
const webGl2Addon = new WebglAddon();
|
||||
webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
|
||||
instance?.loadAddon(webGl2Addon);
|
||||
instance.loadAddon(webGl2Addon);
|
||||
}
|
||||
|
||||
const handleResize = () => fitAddon.fit();
|
||||
|
@ -158,13 +158,11 @@ function Terminal({
|
|||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [ref, instance, dataChannel]);
|
||||
}, [ref, instance]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
onKeyUp={e => e.stopPropagation()}
|
||||
>
|
||||
<div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
@ -17,7 +17,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||
className={cx(
|
||||
"relative w-full",
|
||||
"invalid-within::ring-2 invalid-within::ring-red-600 invalid-within::ring-offset-2",
|
||||
"focus-within:border-slate-300 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 dark:focus-within:border-slate-600",
|
||||
"focus-within:border-slate-300 focus-within:outline-hidden focus-within:ring-1 focus-within:ring-blue-700 dark:focus-within:border-slate-600",
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
|
@ -25,7 +25,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||
{...props}
|
||||
id="asd"
|
||||
className={clsx(
|
||||
"block w-full rounded border-transparent bg-transparent text-black placeholder:text-slate-300 focus:ring-0 disabled:pointer-events-none disabled:select-none disabled:bg-slate-50 disabled:text-slate-300 dark:text-white dark:placeholder:text-slate-500 dark:disabled:bg-slate-800 sm:text-sm",
|
||||
"block w-full rounded-sm border-transparent bg-transparent text-black placeholder:text-slate-300 focus:ring-0 disabled:pointer-events-none disabled:select-none disabled:bg-slate-50 disabled:text-slate-300 dark:text-white dark:placeholder:text-slate-500 dark:disabled:bg-slate-800 sm:text-sm",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -3,18 +3,18 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
|||
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { LuPlay } from "react-icons/lu";
|
||||
import { BsMouseFill } from "react-icons/bs";
|
||||
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { BsMouseFill } from "react-icons/bs";
|
||||
|
||||
interface OverlayContentProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
function OverlayContent({ children }: OverlayContentProps) {
|
||||
return (
|
||||
<GridCard cardClassName="h-full pointer-events-auto !outline-none">
|
||||
<GridCard cardClassName="h-full pointer-events-auto !outline-hidden">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded-md border border-slate-800/30 dark:border-slate-300/20">
|
||||
{children}
|
||||
</div>
|
||||
|
@ -242,8 +242,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
|||
Ensure source device is powered on and outputting a signal
|
||||
</li>
|
||||
<li>
|
||||
If using an adapter, ensure it's compatible and
|
||||
functioning correctly
|
||||
If using an adapter, ensure it's compatible and functioning
|
||||
correctly
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -377,7 +377,7 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
|
|||
>
|
||||
<div>
|
||||
<Card className="rounded-b-none shadow-none !outline-0">
|
||||
<div className="flex items-center justify-between border border-slate-800/50 px-4 py-2 outline-0 backdrop-blur-sm dark:border-slate-300/20 dark:bg-slate-800">
|
||||
<div className="flex items-center justify-between border border-slate-800/50 px-4 py-2 outline-0 backdrop-blur-xs dark:border-slate-300/20 dark:bg-slate-800">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
<span className="text-sm text-black dark:text-white">
|
||||
|
|
|
@ -143,6 +143,16 @@ function KeyboardWrapper() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (key === "CtrlAltBackspace") {
|
||||
sendKeyboardEvent(
|
||||
[keys["Backspace"]],
|
||||
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
||||
);
|
||||
|
||||
setTimeout(resetKeyboardState, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isKeyShift || isKeyCaps) {
|
||||
toggleLayout();
|
||||
|
||||
|
@ -257,13 +267,13 @@ function KeyboardWrapper() {
|
|||
buttonTheme={[
|
||||
{
|
||||
class: "combination-key",
|
||||
buttons: "CtrlAltDelete AltMetaEscape",
|
||||
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||
},
|
||||
]}
|
||||
display={keyDisplayMap}
|
||||
layout={{
|
||||
default: [
|
||||
"CtrlAltDelete AltMetaEscape",
|
||||
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
|
||||
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash",
|
||||
|
@ -272,7 +282,7 @@ function KeyboardWrapper() {
|
|||
"ControlLeft AltLeft MetaLeft Space MetaRight AltRight",
|
||||
],
|
||||
shift: [
|
||||
"CtrlAltDelete AltMetaEscape",
|
||||
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
|
||||
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
|
||||
|
@ -282,7 +292,7 @@ function KeyboardWrapper() {
|
|||
],
|
||||
}}
|
||||
disableButtonHold={true}
|
||||
mergeDisplay={true}
|
||||
syncInstanceInputs={true}
|
||||
debug={false}
|
||||
/>
|
||||
|
||||
|
@ -290,34 +300,25 @@ function KeyboardWrapper() {
|
|||
<Keyboard
|
||||
baseClass="simple-keyboard-control"
|
||||
theme="simple-keyboard hg-theme-default hg-layout-default"
|
||||
layoutName={layoutName}
|
||||
onKeyPress={onKeyDown}
|
||||
display={keyDisplayMap}
|
||||
layout={{
|
||||
default: ["Home Pageup", "Delete End Pagedown"],
|
||||
}}
|
||||
display={{
|
||||
Home: "home",
|
||||
Pageup: "pageup",
|
||||
Delete: "delete",
|
||||
End: "end",
|
||||
Pagedown: "pagedown",
|
||||
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"],
|
||||
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"],
|
||||
}}
|
||||
syncInstanceInputs={true}
|
||||
onKeyPress={onKeyDown}
|
||||
mergeDisplay={true}
|
||||
debug={false}
|
||||
/>
|
||||
<Keyboard
|
||||
baseClass="simple-keyboard-arrows"
|
||||
theme="simple-keyboard hg-theme-default hg-layout-default"
|
||||
display={{
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
ArrowUp: "↑",
|
||||
ArrowDown: "↓",
|
||||
}}
|
||||
onKeyPress={onKeyDown}
|
||||
display={keyDisplayMap}
|
||||
layout={{
|
||||
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
|
||||
}}
|
||||
onKeyPress={onKeyDown}
|
||||
syncInstanceInputs={true}
|
||||
debug={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
|
||||
import {
|
||||
useDeviceSettingsStore,
|
||||
|
@ -10,7 +11,6 @@ import {
|
|||
useVideoStore,
|
||||
} from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { useResizeObserver } from "@/hooks/useResizeObserver";
|
||||
import { cx } from "@/cva.config";
|
||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||
import Actionbar from "@components/ActionBar";
|
||||
|
@ -67,7 +67,7 @@ export default function WebRTCVideo() {
|
|||
|
||||
// Video-related
|
||||
useResizeObserver({
|
||||
ref: videoElm,
|
||||
ref: videoElm as React.RefObject<HTMLElement>,
|
||||
onResize: ({ width, height }) => {
|
||||
// This is actually client size, not videoSize
|
||||
if (width && height) {
|
||||
|
@ -151,7 +151,7 @@ export default function WebRTCVideo() {
|
|||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||
if (isKeyboardLockGranted) {
|
||||
if ("keyboard" in navigator) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||
await navigator.keyboard.lock();
|
||||
}
|
||||
}
|
||||
|
@ -330,11 +330,31 @@ export default function WebRTCVideo() {
|
|||
)
|
||||
// Alt: Keep if Alt is pressed or if the key isn't an Alt key
|
||||
// Example: If altKey is true, keep all modifiers
|
||||
// If altKey is false, filter out 0x04 (AltLeft) and 0x40 (AltRight)
|
||||
// If altKey is false, filter out 0x04 (AltLeft)
|
||||
//
|
||||
// But intentionally do not filter out 0x40 (AltRight) to accomodate
|
||||
// Alt Gr (Alt Graph) as a modifier. Oddly, Alt Gr does not declare
|
||||
// itself to be an altKey. For example, the KeyboardEvent for
|
||||
// Alt Gr + 2 has the following structure:
|
||||
// - altKey: false
|
||||
// - code: "Digit2"
|
||||
// - type: [ "keydown" | "keyup" ]
|
||||
//
|
||||
// For context, filteredModifiers aims to keep track which modifiers
|
||||
// are being pressed on the physical keyboard at any point in time.
|
||||
// There is logic in the keyUpHandler and keyDownHandler to add and
|
||||
// remove 0x40 (AltRight) from the list of new modifiers.
|
||||
//
|
||||
// But relying on the two handlers alone to track the state of the
|
||||
// modifier bears the risk that the key up event for Alt Gr could
|
||||
// get lost while the browser window is temporarily out of focus,
|
||||
// which means the Alt Gr key state would then be "stuck". At this
|
||||
// point, we would need to rely on the user to press Alt Gr again
|
||||
// to properly release the state of that modifier.
|
||||
.filter(
|
||||
modifier =>
|
||||
altKey ||
|
||||
(modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]),
|
||||
(modifier !== modifiers["AltLeft"]),
|
||||
)
|
||||
// Meta: Keep if Meta is pressed or if the key isn't a Meta key
|
||||
// Example: If metaKey is true, keep all modifiers
|
||||
|
@ -653,7 +673,7 @@ export default function WebRTCVideo() {
|
|||
]);
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-layout">
|
||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||
<div className="flex min-h-[39.5px] flex-col">
|
||||
<div className="flex flex-col">
|
||||
<fieldset
|
||||
|
@ -679,7 +699,7 @@ export default function WebRTCVideo() {
|
|||
<div className="flex h-full flex-col">
|
||||
<div className="relative flex-grow overflow-hidden">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="grid flex-grow grid-rows-bodyFooter overflow-hidden">
|
||||
<div className="grid flex-grow grid-rows-(--grid-bodyFooter) overflow-hidden">
|
||||
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="relative inline-block">
|
||||
|
@ -704,7 +724,7 @@ export default function WebRTCVideo() {
|
|||
hdmiError ||
|
||||
peerConnectionState !== "connected",
|
||||
"!opacity-60": showPointerLockBar,
|
||||
"animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
|
||||
"animate-slideUpFade border border-slate-800/30 shadow-xs dark:border-slate-300/20":
|
||||
isPlaying,
|
||||
},
|
||||
)}
|
||||
|
@ -712,7 +732,7 @@ export default function WebRTCVideo() {
|
|||
{peerConnection?.connectionState == "connected" && (
|
||||
<div
|
||||
style={{ animationDuration: "500ms" }}
|
||||
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center opacity-0"
|
||||
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center"
|
||||
>
|
||||
<div className="relative h-full w-full rounded-md">
|
||||
<LoadingVideoOverlay show={isVideoLoading} />
|
||||
|
|
|
@ -107,7 +107,7 @@ export function ATXPowerControl() {
|
|||
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="h-[120px] animate-fadeIn opacity-0">
|
||||
<Card className="h-[120px] animate-fadeIn">
|
||||
<div className="space-y-4 p-3">
|
||||
{/* Control Buttons */}
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
|
@ -63,7 +63,7 @@ export function DCPowerControl() {
|
|||
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="h-[160px] animate-fadeIn opacity-0">
|
||||
<Card className="h-[160px] animate-fadeIn">
|
||||
<div className="space-y-4 p-3">
|
||||
{/* Power Controls */}
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
|
@ -58,7 +58,7 @@ export function SerialConsole() {
|
|||
description="Configure your serial console settings"
|
||||
/>
|
||||
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<Card className="animate-fadeIn">
|
||||
<div className="space-y-4 p-3">
|
||||
{/* Open Console Button */}
|
||||
<div className="flex items-center">
|
||||
|
|
|
@ -84,7 +84,7 @@ export default function ExtensionPopover() {
|
|||
return (
|
||||
<GridCard>
|
||||
<div className="space-y-4 p-4 py-3">
|
||||
<div className="grid h-full grid-rows-headerBody">
|
||||
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||
<div className="space-y-4">
|
||||
{activeExtension ? (
|
||||
// Extension Control View
|
||||
|
@ -92,7 +92,7 @@ export default function ExtensionPopover() {
|
|||
{renderActiveExtension()}
|
||||
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
@ -113,7 +113,7 @@ export default function ExtensionPopover() {
|
|||
title="Extensions"
|
||||
description="Load and manage your extensions"
|
||||
/>
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<Card className="animate-fadeIn">
|
||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||
{AVAILABLE_EXTENSIONS.map(extension => (
|
||||
<div
|
||||
|
|
|
@ -194,7 +194,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
return (
|
||||
<GridCard>
|
||||
<div className="space-y-4 p-4 py-3">
|
||||
<div ref={ref} className="grid h-full grid-rows-headerBody">
|
||||
<div ref={ref} className="grid h-full grid-rows-(--grid-headerBody)">
|
||||
<div className="h-full space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
|
@ -214,7 +214,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
) : null}
|
||||
|
||||
<div
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
className="animate-fadeIn space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -289,7 +289,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
|
||||
{!remoteVirtualMediaState && (
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
|
|
@ -8,14 +8,21 @@ import { GridCard } from "@components/Card";
|
|||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores";
|
||||
import { chars, keys, modifiers } from "@/keyboardMappings";
|
||||
import { useHidStore, useRTCStore, useUiStore, useDeviceSettingsStore } from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { layouts, chars } from "@/keyboardLayouts";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
const hidKeyboardPayload = (keys: number[], modifier: number) => {
|
||||
return { keys, modifier };
|
||||
const hidKeyboardPayload = (keys: number[], modifier: number, hold: boolean) => {
|
||||
return { keys, modifier, hold };
|
||||
};
|
||||
|
||||
const modifierCode = (shift?: boolean, altRight?: boolean) => {
|
||||
return (shift ? modifiers["ShiftLeft"] : 0)
|
||||
| (altRight ? modifiers["AltRight"] : 0)
|
||||
}
|
||||
const noModifier = 0
|
||||
|
||||
export default function PasteModal() {
|
||||
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
|
||||
|
@ -27,6 +34,18 @@ export default function PasteModal() {
|
|||
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
||||
const close = useClose();
|
||||
|
||||
const keyboardLayout = useDeviceSettingsStore(state => state.keyboardLayout);
|
||||
const setKeyboardLayout = useDeviceSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
send("getKeyboardLayout", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onCancelPasteMode = useCallback(() => {
|
||||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
@ -37,27 +56,40 @@ export default function PasteModal() {
|
|||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||
if (!keyboardLayout) return;
|
||||
if (!chars[keyboardLayout]) return;
|
||||
|
||||
const text = TextAreaRef.current.value;
|
||||
|
||||
try {
|
||||
for (const char of text) {
|
||||
const { key, shift } = chars[char] ?? {};
|
||||
const { key, shift, altRight, deadKey, accentKey } = chars[keyboardLayout][char]
|
||||
if (!key) continue;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||
const keyz = [ keys[key] ];
|
||||
const modz = [ modifierCode(shift, altRight) ];
|
||||
|
||||
if (deadKey) {
|
||||
keyz.push(keys["Space"]);
|
||||
modz.push(noModifier);
|
||||
}
|
||||
if (accentKey) {
|
||||
keyz.unshift(keys[accentKey.key])
|
||||
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
|
||||
}
|
||||
|
||||
for (const [index, kei] of keyz.entries()) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload([kei], modz[index], false),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -74,7 +106,7 @@ export default function PasteModal() {
|
|||
return (
|
||||
<GridCard>
|
||||
<div className="space-y-4 p-4 py-3">
|
||||
<div className="grid h-full grid-rows-headerBody">
|
||||
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||
<div className="h-full space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
|
@ -83,7 +115,7 @@ export default function PasteModal() {
|
|||
/>
|
||||
|
||||
<div
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
className="animate-fadeIn space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -113,7 +145,7 @@ export default function PasteModal() {
|
|||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||
[...new Intl.Segmenter().segment(value)]
|
||||
.map(x => x.segment)
|
||||
.filter(char => !chars[char]),
|
||||
.filter(char => !chars[keyboardLayout][char]),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -132,12 +164,17 @@ export default function PasteModal() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Sending key codes using keyboard layout {layouts[keyboardLayout]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end gap-x-2 opacity-0"
|
||||
className="flex animate-fadeIn items-center justify-end gap-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function AddDeviceForm({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="animate-fadeIn space-y-4 opacity-0"
|
||||
className="animate-fadeIn space-y-4"
|
||||
style={{
|
||||
animationDuration: "0.5s",
|
||||
animationFillMode: "forwards",
|
||||
|
@ -73,7 +73,7 @@ export default function AddDeviceForm({
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
|
|
@ -28,7 +28,7 @@ export default function DeviceList({
|
|||
}: DeviceListProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<Card className="animate-fadeIn">
|
||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||
{storedDevices.map((device, index) => (
|
||||
<div key={index} className="flex items-center justify-between gap-x-2 p-3">
|
||||
|
@ -63,7 +63,7 @@ export default function DeviceList({
|
|||
</div>
|
||||
</Card>
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function EmptyStateCard({
|
|||
}) {
|
||||
return (
|
||||
<div className="select-none space-y-4">
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<Card className="animate-fadeIn">
|
||||
<div className="flex items-center justify-center py-8 text-center">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
|
@ -35,7 +35,7 @@ export default function EmptyStateCard({
|
|||
</div>
|
||||
</Card>
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
|
|
@ -102,7 +102,7 @@ export default function WakeOnLanModal() {
|
|||
return (
|
||||
<GridCard>
|
||||
<div className="space-y-4 p-4 py-3">
|
||||
<div className="grid h-full grid-rows-headerBody">
|
||||
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Wake On LAN"
|
||||
|
|
|
@ -99,7 +99,7 @@ export default function ConnectionStatsSidebar() {
|
|||
}, 500);
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-rows-headerBody shadow-sm">
|
||||
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
|
||||
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
|
||||
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
|
||||
<div className="space-y-4">
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import { MAX_STEPS_PER_MACRO, MAX_TOTAL_MACROS, MAX_KEYS_PER_STEP } from "@/constants/macros";
|
||||
|
||||
import {
|
||||
MAX_STEPS_PER_MACRO,
|
||||
MAX_TOTAL_MACROS,
|
||||
MAX_KEYS_PER_STEP,
|
||||
} from "@/constants/macros";
|
||||
|
||||
// Define the JsonRpc types for better type checking
|
||||
interface JsonRpcResponse {
|
||||
|
@ -292,6 +297,9 @@ interface SettingsState {
|
|||
developerMode: boolean;
|
||||
setDeveloperMode: (enabled: boolean) => void;
|
||||
|
||||
displayRotation: string;
|
||||
setDisplayRotation: (rotation: string) => void;
|
||||
|
||||
backlightSettings: BacklightSettings;
|
||||
setBacklightSettings: (settings: BacklightSettings) => void;
|
||||
}
|
||||
|
@ -312,6 +320,10 @@ export const useSettingsStore = create(
|
|||
developerMode: false,
|
||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||
|
||||
displayRotation: "270",
|
||||
setDisplayRotation: (rotation: string) =>
|
||||
set({ displayRotation: rotation }),
|
||||
|
||||
backlightSettings: {
|
||||
max_brightness: 100,
|
||||
dim_after: 10000,
|
||||
|
@ -336,6 +348,8 @@ export interface DeviceSettingsState {
|
|||
trackpadThreshold: number;
|
||||
scrollSensitivity: "low" | "default" | "high";
|
||||
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void;
|
||||
keyboardLayout: string;
|
||||
setKeyboardLayout: (layout: string) => void;
|
||||
}
|
||||
|
||||
export const useDeviceSettingsStore = create<DeviceSettingsState>(set => ({
|
||||
|
@ -397,6 +411,9 @@ export const useDeviceSettingsStore = create<DeviceSettingsState>(set => ({
|
|||
scrollSensitivity: sensitivity,
|
||||
});
|
||||
},
|
||||
|
||||
keyboardLayout: "en_US",
|
||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||
}));
|
||||
|
||||
export interface RemoteVirtualMediaState {
|
||||
|
@ -564,12 +581,12 @@ export interface UpdateState {
|
|||
setOtaState: (state: UpdateState["otaState"]) => void;
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||
modalView:
|
||||
| "loading"
|
||||
| "updating"
|
||||
| "upToDate"
|
||||
| "updateAvailable"
|
||||
| "updateCompleted"
|
||||
| "error";
|
||||
| "loading"
|
||||
| "updating"
|
||||
| "upToDate"
|
||||
| "updateAvailable"
|
||||
| "updateCompleted"
|
||||
| "error";
|
||||
setModalView: (view: UpdateState["modalView"]) => void;
|
||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||
updateErrorMessage: string | null;
|
||||
|
@ -633,12 +650,12 @@ export const useUsbConfigModalStore = create<UsbConfigModalState>(set => ({
|
|||
|
||||
interface LocalAuthModalState {
|
||||
modalView:
|
||||
| "createPassword"
|
||||
| "deletePassword"
|
||||
| "updatePassword"
|
||||
| "creationSuccess"
|
||||
| "deleteSuccess"
|
||||
| "updateSuccess";
|
||||
| "createPassword"
|
||||
| "deletePassword"
|
||||
| "updatePassword"
|
||||
| "creationSuccess"
|
||||
| "deleteSuccess"
|
||||
| "updateSuccess";
|
||||
setModalView: (view: LocalAuthModalState["modalView"]) => void;
|
||||
}
|
||||
|
||||
|
@ -719,12 +736,23 @@ export interface NetworkState {
|
|||
setDhcpLeaseExpiry: (expiry: Date) => void;
|
||||
}
|
||||
|
||||
|
||||
export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown";
|
||||
export type IPv6Mode =
|
||||
| "disabled"
|
||||
| "slaac"
|
||||
| "dhcpv6"
|
||||
| "slaac_and_dhcpv6"
|
||||
| "static"
|
||||
| "link_local"
|
||||
| "unknown";
|
||||
export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown";
|
||||
export type LLDPMode = "disabled" | "basic" | "all" | "unknown";
|
||||
export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown";
|
||||
export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown";
|
||||
export type TimeSyncMode =
|
||||
| "ntp_only"
|
||||
| "ntp_and_http"
|
||||
| "http_only"
|
||||
| "custom"
|
||||
| "unknown";
|
||||
|
||||
export interface NetworkSettings {
|
||||
hostname: string;
|
||||
|
@ -749,7 +777,7 @@ export const useNetworkStateStore = create<NetworkState>((set, get) => ({
|
|||
|
||||
lease.lease_expiry = expiry;
|
||||
set({ dhcp_lease: lease });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export interface KeySequenceStep {
|
||||
|
@ -771,8 +799,20 @@ export interface MacrosState {
|
|||
initialized: boolean;
|
||||
loadMacros: () => Promise<void>;
|
||||
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
||||
sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void) | null;
|
||||
setSendFn: (sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void)) => void;
|
||||
sendFn:
|
||||
| ((
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void)
|
||||
| null;
|
||||
setSendFn: (
|
||||
sendFn: (
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const generateMacroId = () => {
|
||||
|
@ -785,7 +825,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
initialized: false,
|
||||
sendFn: null,
|
||||
|
||||
setSendFn: (sendFn) => {
|
||||
setSendFn: sendFn => {
|
||||
set({ sendFn });
|
||||
},
|
||||
|
||||
|
@ -802,7 +842,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
sendFn("getKeyboardMacros", {}, (response) => {
|
||||
sendFn("getKeyboardMacros", {}, response => {
|
||||
if (response.error) {
|
||||
console.error("Error loading macros:", response.error);
|
||||
reject(new Error(response.error.message));
|
||||
|
@ -822,7 +862,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
|
||||
set({
|
||||
macros: sortedMacros,
|
||||
initialized: true
|
||||
initialized: true,
|
||||
});
|
||||
|
||||
resolve();
|
||||
|
@ -849,15 +889,23 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
|
||||
for (const macro of macros) {
|
||||
if (macro.steps.length > MAX_STEPS_PER_MACRO) {
|
||||
console.error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`);
|
||||
throw new Error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`);
|
||||
console.error(
|
||||
`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`,
|
||||
);
|
||||
throw new Error(
|
||||
`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < macro.steps.length; i++) {
|
||||
const step = macro.steps[i];
|
||||
if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) {
|
||||
console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
||||
throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
||||
console.error(
|
||||
`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`,
|
||||
);
|
||||
throw new Error(
|
||||
`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -867,20 +915,25 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
try {
|
||||
const macrosWithSortOrder = macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index
|
||||
sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index,
|
||||
}));
|
||||
|
||||
const response = await new Promise<JsonRpcResponse>((resolve) => {
|
||||
sendFn("setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, (response) => {
|
||||
resolve(response);
|
||||
});
|
||||
const response = await new Promise<JsonRpcResponse>(resolve => {
|
||||
sendFn(
|
||||
"setKeyboardMacros",
|
||||
{ params: { macros: macrosWithSortOrder } },
|
||||
response => {
|
||||
resolve(response);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error("Error saving macros:", response.error);
|
||||
const errorMessage = typeof response.error.data === 'string'
|
||||
? response.error.data
|
||||
: response.error.message || "Failed to save macros";
|
||||
const errorMessage =
|
||||
typeof response.error.data === "string"
|
||||
? response.error.data
|
||||
: response.error.message || "Failed to save macros";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
|
@ -892,5 +945,6 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function useInterval(callback: () => void, delay: number) {
|
||||
const savedCallback = useRef<typeof callback>();
|
||||
|
||||
// Save the callback directly in the useRef object
|
||||
savedCallback.current = callback;
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
if (!savedCallback.current) return;
|
||||
savedCallback.current();
|
||||
}
|
||||
|
||||
if (delay !== null) {
|
||||
const id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Custom hook that determines if the component is currently mounted.
|
||||
* @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted.
|
||||
* @public
|
||||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted)
|
||||
* @example
|
||||
* ```tsx
|
||||
* const isComponentMounted = useIsMounted();
|
||||
* // Use isComponentMounted() to check if the component is currently mounted before performing certain actions.
|
||||
* ```
|
||||
*/
|
||||
export function useIsMounted(): () => boolean {
|
||||
const isMounted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useCallback(() => isMounted.current, []);
|
||||
}
|
|
@ -4,6 +4,10 @@ import { useHidStore, useRTCStore } from "@/hooks/stores";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
|
||||
const hidKeyboardPayload = (keys: number[], modifier: number, hold: boolean) => {
|
||||
return { keys, modifier, hold };
|
||||
};
|
||||
|
||||
export default function useKeyboard() {
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
|
@ -17,7 +21,7 @@ export default function useKeyboard() {
|
|||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
const accModifier = modifiers.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
send("keyboardReport", { keys, modifier: accModifier });
|
||||
send("keyboardReport", hidKeyboardPayload(keys, accModifier, true));
|
||||
|
||||
// We do this for the info bar to display the currently pressed keys for the user
|
||||
updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers });
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import type { RefObject } from "react";
|
||||
|
||||
import { useIsMounted } from "./useIsMounted";
|
||||
|
||||
/** The size of the observed element. */
|
||||
interface Size {
|
||||
/** The width of the observed element. */
|
||||
width: number | undefined;
|
||||
/** The height of the observed element. */
|
||||
height: number | undefined;
|
||||
}
|
||||
|
||||
/** The options for the ResizeObserver. */
|
||||
interface UseResizeObserverOptions<T extends HTMLElement = HTMLElement> {
|
||||
/** The ref of the element to observe. */
|
||||
ref: RefObject<T>;
|
||||
/**
|
||||
* When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.
|
||||
* @default undefined
|
||||
*/
|
||||
onResize?: (size: Size) => void;
|
||||
/**
|
||||
* The box model to use for the ResizeObserver.
|
||||
* @default 'content-box'
|
||||
*/
|
||||
box?: "border-box" | "content-box" | "device-pixel-content-box";
|
||||
}
|
||||
|
||||
const initialSize: Size = {
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook that observes the size of an element using the ResizeObserver API.
|
||||
* @template T - The type of the element to observe.
|
||||
* @param {UseResizeObserverOptions<T>} options - The options for the ResizeObserver.
|
||||
* @returns {Size} - The size of the observed element.
|
||||
* @public
|
||||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-resize-observer)
|
||||
* @see [MDN ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
||||
* @example
|
||||
* ```tsx
|
||||
* const myRef = useRef(null);
|
||||
* const { width = 0, height = 0 } = useResizeObserver({
|
||||
* ref: myRef,
|
||||
* box: 'content-box',
|
||||
* });
|
||||
*
|
||||
* <div ref={myRef}>Hello, world!</div>
|
||||
* ```
|
||||
*/
|
||||
export function useResizeObserver<T extends HTMLElement = HTMLElement>(
|
||||
options: UseResizeObserverOptions<T>,
|
||||
): Size {
|
||||
const { ref, box = "content-box" } = options;
|
||||
const [{ width, height }, setSize] = useState<Size>(initialSize);
|
||||
const isMounted = useIsMounted();
|
||||
const previousSize = useRef<Size>({ ...initialSize });
|
||||
const onResize = useRef<((size: Size) => void) | undefined>(undefined);
|
||||
onResize.current = options.onResize;
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
if (typeof window === "undefined" || !("ResizeObserver" in window)) return;
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
const boxProp =
|
||||
box === "border-box"
|
||||
? "borderBoxSize"
|
||||
: box === "device-pixel-content-box"
|
||||
? "devicePixelContentBoxSize"
|
||||
: "contentBoxSize";
|
||||
|
||||
const newWidth = extractSize(entry, boxProp, "inlineSize");
|
||||
const newHeight = extractSize(entry, boxProp, "blockSize");
|
||||
|
||||
const hasChanged =
|
||||
previousSize.current.width !== newWidth ||
|
||||
previousSize.current.height !== newHeight;
|
||||
|
||||
if (hasChanged) {
|
||||
const newSize: Size = { width: newWidth, height: newHeight };
|
||||
previousSize.current.width = newWidth;
|
||||
previousSize.current.height = newHeight;
|
||||
|
||||
if (onResize.current) {
|
||||
onResize.current(newSize);
|
||||
} else {
|
||||
if (isMounted()) {
|
||||
setSize(newSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(ref.current, { box });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [box, isMounted, ref]);
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
/** @private */
|
||||
type BoxSizesKey = keyof Pick<
|
||||
ResizeObserverEntry,
|
||||
"borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize"
|
||||
>;
|
||||
|
||||
function extractSize(
|
||||
entry: ResizeObserverEntry,
|
||||
box: BoxSizesKey,
|
||||
sizeType: keyof ResizeObserverSize,
|
||||
): number | undefined {
|
||||
if (!entry[box]) {
|
||||
if (box === "contentBoxSize") {
|
||||
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Array.isArray(entry[box])
|
||||
? entry[box][0][sizeType]
|
||||
: // @ts-expect-error Support Firefox's non-standard behavior
|
||||
(entry[box][sizeType] as number);
|
||||
}
|
137
ui/src/index.css
137
ui/src/index.css
|
@ -1,6 +1,11 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
@config "../tailwind.config.js";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "@tailwindcss/forms";
|
||||
@plugin "@headlessui/tailwindcss";
|
||||
|
||||
/* Dark mode uses CSS selector instead of prefers-color-scheme */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
html {
|
||||
@apply scroll-smooth;
|
||||
|
@ -13,6 +18,128 @@ body {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: "Circular", sans-serif;
|
||||
--font-display: "Circular", sans-serif;
|
||||
--font-serif: "Circular", serif;
|
||||
--font-mono: "Source Code Pro Variable", monospace;
|
||||
|
||||
--grid-layout: auto 1fr auto;
|
||||
--grid-headerBody: auto 1fr;
|
||||
--grid-bodyFooter: 1fr auto;
|
||||
--grid-sidebar: 1fr minmax(360px, 25%);
|
||||
|
||||
--breakpoint-xs: 480px;
|
||||
--breakpoint-2xl: 1440px;
|
||||
--breakpoint-3xl: 1920px;
|
||||
--breakpoint-4xl: 2560px;
|
||||
|
||||
/* Migrated animations */
|
||||
--animate-enter: enter 0.2s ease-out;
|
||||
--animate-leave: leave 0.15s ease-in forwards;
|
||||
--animate-fadeInScale: fadeInScale 1s ease-out forwards;
|
||||
--animate-fadeInScaleFloat:
|
||||
fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite;
|
||||
--animate-fadeIn: fadeIn 1s ease-out forwards;
|
||||
--animate-slideUpFade: slideUpFade 1s ease-out forwards;
|
||||
|
||||
--container-8xl: 88rem;
|
||||
--container-9xl: 96rem;
|
||||
--container-10xl: 104rem;
|
||||
--container-11xl: 112rem;
|
||||
--container-12xl: 120rem;
|
||||
|
||||
/* Migrated keyframes */
|
||||
@keyframes enter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leave {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInScaleFloat {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.98) translateY(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.8;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUpFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@utility max-width-* {
|
||||
max-width: --modifier(--container- *, [length], [ *]);
|
||||
}
|
||||
|
||||
/* Ensure there is not a `ms` and ms -> `...)ms` */
|
||||
@utility animation-delay-* {
|
||||
animation-delay: --value(integer) ms;
|
||||
}
|
||||
|
||||
@property --grid-color-start {
|
||||
syntax: "<color>";
|
||||
initial-value: theme("colors.blue.50/10");
|
||||
|
@ -50,7 +177,7 @@ video::-webkit-media-controls {
|
|||
}
|
||||
|
||||
.hg-theme-default .hg-button {
|
||||
@apply border !border-b border-slate-800/25 !border-b-slate-800/25 !shadow-sm;
|
||||
@apply border !border-b border-slate-800/25 !border-b-slate-800/25 !shadow-xs;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-button span {
|
||||
|
@ -174,7 +301,7 @@ video::-webkit-media-controls {
|
|||
}
|
||||
|
||||
.hg-theme-default .hg-row .combination-key {
|
||||
@apply inline-flex !h-auto !w-auto flex-grow-0 py-1 text-xs;
|
||||
@apply inline-flex !h-auto !w-auto grow-0 py-1 text-xs;
|
||||
}
|
||||
|
||||
.hg-theme-default .hg-row:has(.combination-key) {
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
|
||||
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
|
||||
|
||||
type KeyInfo = { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
|
||||
export const layouts: Record<string, string> = {
|
||||
cs_CZ: name_cs_CZ,
|
||||
en_UK: name_en_UK,
|
||||
en_US: name_en_US,
|
||||
fr_FR: name_fr_FR,
|
||||
de_DE: name_de_DE,
|
||||
it_IT: name_it_IT,
|
||||
nb_NO: name_nb_NO,
|
||||
es_ES: name_es_ES,
|
||||
sv_SE: name_sv_SE,
|
||||
fr_CH: name_fr_CH,
|
||||
de_CH: name_de_CH,
|
||||
}
|
||||
|
||||
export const chars: Record<string, Record<string, KeyCombo>> = {
|
||||
cs_CZ: chars_cs_CZ,
|
||||
en_UK: chars_en_UK,
|
||||
en_US: chars_en_US,
|
||||
fr_FR: chars_fr_FR,
|
||||
de_DE: chars_de_DE,
|
||||
it_IT: chars_it_IT,
|
||||
nb_NO: chars_nb_NO,
|
||||
es_ES: chars_es_ES,
|
||||
sv_SE: chars_sv_SE,
|
||||
fr_CH: chars_fr_CH,
|
||||
de_CH: chars_de_CH,
|
||||
};
|
|
@ -0,0 +1,244 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Čeština";
|
||||
|
||||
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Digit3", shift: true, altRight: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyCaron = { key: "Equal", shift: true } // caron or haček (inverted hat), mark ˇ placed above the letter
|
||||
const keyGrave = { key: "Digit7", shift: true, altRight: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Digit1", shift: true, altRight: true } // tilde, mark ~ placed above the letter
|
||||
const keyRing = { key: "Backquote", shift: true } // kroužek (little ring), mark ° placed above the letter
|
||||
const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (dot above), mark ˙ placed above the letter
|
||||
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
|
||||
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
"Ȧ": { key: "KeyA", shift: true, accentKey: keyOverdot },
|
||||
"Ą": { key: "KeyA", shift: true, accentKey: keyHook },
|
||||
B: { key: "KeyB", shift: true },
|
||||
"Ḃ": { key: "KeyB", shift: true, accentKEy: keyOverdot },
|
||||
C: { key: "KeyC", shift: true },
|
||||
"Č": { key: "KeyC", shift: true, accentKey: keyCaron },
|
||||
"Ċ": { key: "KeyC", shift: true, accentKey: keyOverdot },
|
||||
"Ç": { key: "KeyC", shift: true, accentKey: keyCedille },
|
||||
D: { key: "KeyD", shift: true },
|
||||
"Ď": { key: "KeyD", shift: true, accentKey: keyCaron },
|
||||
"Ḋ": { key: "KeyD", shift: true, accentKey: keyOverdot },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"Ě": { key: "KeyE", shift: true, accentKey: keyCaron },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
"Ė": { key: "KeyE", shift: true, accentKEy: keyOverdot },
|
||||
"Ę": { key: "KeyE", shift: true, accentKey: keyHook },
|
||||
F: { key: "KeyF", shift: true },
|
||||
"Ḟ": { key: "KeyF", shift: true, accentKey: keyOverdot },
|
||||
G: { key: "KeyG", shift: true },
|
||||
"Ġ": { key: "KeyG", shift: true, accentKey: keyOverdot },
|
||||
H: { key: "KeyH", shift: true },
|
||||
"Ḣ": { key: "KeyH", shift: true, accentKey: keyOverdot },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
"İ": { key: "KeyI", shift: true, accentKey: keyOverdot },
|
||||
"Į": { key: "KeyI", shift: true, accentKey: keyHook },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
"Ŀ": { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
"Ṁ": { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
"Ň": { key: "KeyN", shift: true, accentKey: keyCaron },
|
||||
"Ñ": { key: "KeyN", shift: true, accentKey: keyTilde },
|
||||
"Ṅ": { key: "KeyN", shift: true, accentKEy: keyOverdot },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
"Ȯ": { key: "KeyO", shift: true, accentKey: keyOverdot },
|
||||
"Ǫ": { key: "KeyO", shift: true, accentKey: keyHook },
|
||||
P: { key: "KeyP", shift: true },
|
||||
"Ṗ": { key: "KeyP", shift: true, accentKey: keyOverdot },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
"Ř": { key: "KeyR", shift: true, accentKey: keyCaron },
|
||||
"Ṙ": { key: "KeyR", shift: true, accentKey: keyOverdot },
|
||||
S: { key: "KeyS", shift: true },
|
||||
"Š": { key: "KeyS", shift: true, accentKey: keyCaron },
|
||||
"Ṡ": { key: "KeyS", shift: true, accentKey: keyOverdot },
|
||||
T: { key: "KeyT", shift: true },
|
||||
"Ť": { key: "KeyT", shift: true, accentKey: keyCaron },
|
||||
"Ṫ": { key: "KeyT", shift: true, accentKey: keyOverdot },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
"Ů": { key: "KeyU", shift: true, accentKey: keyRing },
|
||||
"Ų": { key: "KeyU", shift: true, accentKey: keyHook },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
"Ẇ": { key: "KeyW", shift: true, accentKey: keyOverdot },
|
||||
X: { key: "KeyX", shift: true },
|
||||
"Ẋ": { key: "KeyX", shift: true, accentKey: keyOverdot },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
"Ý": { key: "KeyY", shift: true, accentKey: keyAcute },
|
||||
"Ẏ": { key: "KeyY", shift: true, accentKey: keyOverdot },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
"Ż": { key: "KeyZ", shift: true, accentKey: keyOverdot },
|
||||
a: { key: "KeyA" },
|
||||
"ä": { key: "KeyA", accentKey: keyTrema },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
"ȧ": { key: "KeyA", accentKey: keyOverdot },
|
||||
"ą": { key: "KeyA", accentKey: keyHook },
|
||||
b: { key: "KeyB" },
|
||||
"{": { key: "KeyB", altRight: true },
|
||||
"ḃ": { key: "KeyB", accentKey: keyOverdot },
|
||||
c: { key: "KeyC" },
|
||||
"&": { key: "KeyC", altRight: true },
|
||||
"ç": { key: "KeyC", accentKey: keyCedille },
|
||||
"ċ": { key: "KeyC", accentKey: keyOverdot },
|
||||
d: { key: "KeyD" },
|
||||
"ď": { key: "KeyD", accentKey: keyCaron },
|
||||
"ḋ": { key: "KeyD", accentKey: keyOverdot },
|
||||
"Đ": { key: "KeyD", altRight: true },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ė": { key: "KeyE", accentKey: keyOverdot },
|
||||
"ę": { key: "KeyE", accentKey: keyHook },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
"ḟ": { key: "KeyF", accentKey: keyOverdot },
|
||||
"[": { key: "KeyF", altRight: true },
|
||||
g: { key: "KeyG" },
|
||||
"ġ": { key: "KeyG", accentKey: keyOverdot },
|
||||
"]": { key: "KeyF", altRight: true },
|
||||
h: { key: "KeyH" },
|
||||
"ḣ": { key: "KeyH", accentKey: keyOverdot },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
"ı": { key: "KeyI", accentKey: keyOverdot },
|
||||
"į": { key: "KeyI", accentKey: keyHook },
|
||||
j: { key: "KeyJ" },
|
||||
"ȷ": { key: "KeyJ", accentKey: keyOverdot },
|
||||
k: { key: "KeyK" },
|
||||
"ł": { key: "KeyK", altRight: true },
|
||||
l: { key: "KeyL" },
|
||||
"ŀ": { key: "KeyL", accentKey: keyOverdot },
|
||||
"Ł": { key: "KeyL", altRight: true },
|
||||
m: { key: "KeyM" },
|
||||
"ṁ": { key: "KeyM", accentKey: keyOverdot },
|
||||
n: { key: "KeyN" },
|
||||
"}": { key: "KeyN", altRight: true },
|
||||
"ň": { key: "KeyN", accentKey: keyCaron },
|
||||
"ñ": { key: "KeyN", accentKey: keyTilde },
|
||||
"ṅ": { key: "KeyN", accentKey: keyOverdot },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "Key0", accentKey: keyTrema },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
"ȯ": { key: "KeyO", accentKey: keyOverdot },
|
||||
"ǫ": { key: "KeyO", accentKey: keyHook },
|
||||
p: { key: "KeyP" },
|
||||
"ṗ": { key: "KeyP", accentKey: keyOverdot },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
"ṙ": { key: "KeyR", accentKey: keyOverdot },
|
||||
s: { key: "KeyS" },
|
||||
"ṡ": { key: "KeyS", accentKey: keyOverdot },
|
||||
"đ": { key: "KeyS", altRight: true },
|
||||
t: { key: "KeyT" },
|
||||
"ť": { key: "KeyT", accentKey: keyCaron },
|
||||
"ṫ": { key: "KeyT", accentKey: keyOverdot },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
"ų": { key: "KeyU", accentKey: keyHook },
|
||||
v: { key: "KeyV" },
|
||||
"@": { key: "KeyV", altRight: true },
|
||||
w: { key: "KeyW" },
|
||||
"ẇ": { key: "KeyW", accentKey: keyOverdot },
|
||||
x: { key: "KeyX" },
|
||||
"#": { key: "KeyX", altRight: true },
|
||||
"ẋ": { key: "KeyX", accentKey: keyOverdot },
|
||||
y: { key: "KeyY" },
|
||||
"ẏ": { key: "KeyY", accentKey: keyOverdot },
|
||||
z: { key: "KeyZ" },
|
||||
"ż": { key: "KeyZ", accentKey: keyOverdot },
|
||||
";": { key: "Backquote" },
|
||||
"°": { key: "Backquote", shift: true, deadKey: true },
|
||||
"+": { key: "Digit1" },
|
||||
1: { key: "Digit1", shift: true },
|
||||
"ě": { key: "Digit2" },
|
||||
2: { key: "Digit2", shift: true },
|
||||
"š": { key: "Digit3" },
|
||||
3: { key: "Digit3", shift: true },
|
||||
"č": { key: "Digit4" },
|
||||
4: { key: "Digit4", shift: true },
|
||||
"ř": { key: "Digit5" },
|
||||
5: { key: "Digit5", shift: true },
|
||||
"ž": { key: "Digit6" },
|
||||
6: { key: "Digit6", shift: true },
|
||||
"ý": { key: "Digit7" },
|
||||
7: { key: "Digit7", shift: true },
|
||||
"á": { key: "Digit8" },
|
||||
8: { key: "Digit8", shift: true },
|
||||
"í": { key: "Digit9" },
|
||||
9: { key: "Digit9", shift: true },
|
||||
"é": { key: "Digit0" },
|
||||
0: { key: "Digit0", shift: true },
|
||||
"=": { key: "Minus" },
|
||||
"%": { key: "Minus", shift: true },
|
||||
"ú": { key: "BracketLeft" },
|
||||
"/": { key: "BracketLeft", shift: true },
|
||||
")": { key: "BracketRight" },
|
||||
"(": { key: "BracketRight", shift: true },
|
||||
"ů": { key: "Semicolon" },
|
||||
"\"": { key: "Semicolon", shift: true },
|
||||
"§": { key: "Quote" },
|
||||
"!": { key: "Quote", shift: true },
|
||||
"'": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
"?": { key: "Comma", shift: true },
|
||||
"<": { key: "Comma", altRight: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
">": { key: "Period", altRight: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"*": { key: "Slash", altRight: true },
|
||||
"\\": { key: "IntlBackslash" },
|
||||
"|": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,165 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Schwiizerdütsch";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"§": { key: "Backquote" },
|
||||
"°": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"+": { key: "Digit1", shift: true },
|
||||
"|": { key: "Digit1", altRight: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"*": { key: "Digit3", shift: true },
|
||||
"#": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"ç": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"'": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"^": { key: "Equal", deadKey: true },
|
||||
"`": { key: "Equal", shift: true },
|
||||
"~": { key: "Equal", altRight: true, deadKey: true },
|
||||
"ü": { key: "BracketLeft" },
|
||||
"è": { key: "BracketLeft", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"!": { key: "BracketRight", shift: true },
|
||||
"]": { key: "BracketRight", altRight: true },
|
||||
"ö": { key: "Semicolon" },
|
||||
"é": { key: "Semicolon", shift: true },
|
||||
"ä": { key: "Quote" },
|
||||
"à": { key: "Quote", shift: true },
|
||||
"{": { key: "Quote", altRight: true },
|
||||
"$": { key: "Backslash" },
|
||||
"£": { key: "Backslash", shift: true },
|
||||
"}": { key: "Backslash", altRight: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"\\": { key: "IntlBackslash", altRight: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,152 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Deutsch";
|
||||
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave},
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"é": { key: "KeyE", accentKey: keyAcute},
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
"µ": { key: "KeyM", altRight: true },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
"@": { key: "KeyQ", altRight: true },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"°": { key: "Backquote", shift: true },
|
||||
"^": { key: "Backquote", deadKey: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"²": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"§": { key: "Digit3", shift: true },
|
||||
"³": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"$": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
"{": { key: "Digit7", altRight: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
"[": { key: "Digit8", altRight: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
"]": { key: "Digit9", altRight: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"}": { key: "Digit0", altRight: true },
|
||||
"ß": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"\\": { key: "Minus", altRight: true },
|
||||
"´": { key: "Equal", deadKey: true },
|
||||
"`": { key: "Equal", shift: true, deadKey: true },
|
||||
"ü": { key: "BracketLeft" },
|
||||
"Ü": { key: "BracketLeft", shift: true },
|
||||
"+": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", shift: true },
|
||||
"~": { key: "BracketRight", altRight: true },
|
||||
"ö": { key: "Semicolon" },
|
||||
"Ö": { key: "Semicolon", shift: true },
|
||||
"ä": { key: "Quote" },
|
||||
"Ä": { key: "Quote", shift: true },
|
||||
"#": { key: "Backslash" },
|
||||
"'": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"|": { key: "IntlBackslash", altRight: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,107 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (UK)";
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3" },
|
||||
"£": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4" },
|
||||
$: { key: "Digit4", shift: true },
|
||||
"€": { key: "Digit4", altRight: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"^": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"&": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"*": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
"(": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
")": { key: "Digit0", shift: true },
|
||||
"-": { key: "Minus" },
|
||||
_: { key: "Minus", shift: true },
|
||||
"=": { key: "Equal" },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"'": { key: "Quote" },
|
||||
'@': { key: "Quote", shift: true },
|
||||
",": { key: "Comma" },
|
||||
"<": { key: "Comma", shift: true },
|
||||
"/": { key: "Slash" },
|
||||
"?": { key: "Slash", shift: true },
|
||||
".": { key: "Period" },
|
||||
">": { key: "Period", shift: true },
|
||||
";": { key: "Semicolon" },
|
||||
":": { key: "Semicolon", shift: true },
|
||||
"[": { key: "BracketLeft" },
|
||||
"{": { key: "BracketLeft", shift: true },
|
||||
"]": { key: "BracketRight" },
|
||||
"}": { key: "BracketRight", shift: true },
|
||||
"#": { key: "Backslash" },
|
||||
"~": { key: "Backslash", shift: true },
|
||||
"`": { key: "Backquote" },
|
||||
"¬": { key: "Backquote", shift: true },
|
||||
"\\": { key: "IntlBackslash" },
|
||||
"|": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>
|
|
@ -0,0 +1,113 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (US)";
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"@": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3" },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4" },
|
||||
$: { key: "Digit4", shift: true },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"^": { key: "Digit6", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit7", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"*": { key: "Digit8", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit9", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit0", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"-": { key: "Minus" },
|
||||
_: { key: "Minus", shift: true },
|
||||
"=": { key: "Equal" },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"'": { key: "Quote" },
|
||||
'"': { key: "Quote", shift: true },
|
||||
",": { key: "Comma" },
|
||||
"<": { key: "Comma", shift: true },
|
||||
"/": { key: "Slash" },
|
||||
"?": { key: "Slash", shift: true },
|
||||
".": { key: "Period" },
|
||||
">": { key: "Period", shift: true },
|
||||
";": { key: "Semicolon" },
|
||||
":": { key: "Semicolon", shift: true },
|
||||
"[": { key: "BracketLeft" },
|
||||
"{": { key: "BracketLeft", shift: true },
|
||||
"]": { key: "BracketRight" },
|
||||
"}": { key: "BracketRight", shift: true },
|
||||
"\\": { key: "Backslash" },
|
||||
"|": { key: "Backslash", shift: true },
|
||||
"`": { key: "Backquote" },
|
||||
"~": { key: "Backquote", shift: true },
|
||||
"§": { key: "IntlBackslash" },
|
||||
"±": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space", shift: false },
|
||||
"\n": { key: "Enter", shift: false },
|
||||
Enter: { key: "Enter", shift: false },
|
||||
Tab: { key: "Tab", shift: false },
|
||||
PrintScreen: { key: "Prt Sc", shift: false },
|
||||
SystemRequest: { key: "Prt Sc", shift: true },
|
||||
ScrollLock: { key: "ScrollLock", shift: false},
|
||||
Pause: { key: "Pause", shift: false },
|
||||
Break: { key: "Pause", shift: true },
|
||||
Insert: { key: "Insert", shift: false },
|
||||
Delete: { key: "Delete", shift: false },
|
||||
} as Record<string, KeyCombo>
|
|
@ -0,0 +1,168 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Español";
|
||||
|
||||
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"ä": { key: "KeyA", accentKey: keyTrema },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"é": { key: "KeyE", accentKey: keyAcute },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "KeyO", accentKey: keyTrema },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"º": { key: "Backquote" },
|
||||
"ª": { key: "Backquote", shift: true },
|
||||
"\\": { key: "Backquote", altRight: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
"|": { key: "Digit1", altRight: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"·": { key: "Digit3", shift: true },
|
||||
"#": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"$": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
"¬": { key: "Digit6", altRight: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"'": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"¡": { key: "Equal", deadKey: true },
|
||||
"¿": { key: "Equal", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"+": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", shift: true },
|
||||
"]": { key: "BracketRight", altRight: true },
|
||||
"ñ": { key: "Semicolon" },
|
||||
"Ñ": { key: "Semicolon", shift: true },
|
||||
"{": { key: "Quote", altRight: true },
|
||||
"ç": { key: "Backslash" },
|
||||
"Ç": { key: "Backslash", shift: true },
|
||||
"}": { key: "Backslash", altRight: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,14 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { chars as chars_de_CH } from "./de_CH"
|
||||
|
||||
export const name = "Français de Suisse";
|
||||
|
||||
export const chars = {
|
||||
...chars_de_CH,
|
||||
"è": { key: "BracketLeft" },
|
||||
"ü": { key: "BracketLeft", shift: true },
|
||||
"é": { key: "Semicolon" },
|
||||
"ö": { key: "Semicolon", shift: true },
|
||||
"à": { key: "Quote" },
|
||||
"ä": { key: "Quote", shift: true },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,139 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Français";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "Semicolon", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyA", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyZ", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyW", shift: true },
|
||||
a: { key: "KeyQ" },
|
||||
"ä": { key: "KeyQ", accentKey: keyTrema },
|
||||
"â": { key: "KeyQ", accentKey: keyHat },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "Semicolon" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "KeyO", accentKey: keyTrema },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyA" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyZ" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyW" },
|
||||
"²": { key: "Backquote" },
|
||||
"&": { key: "Digit1" },
|
||||
1: { key: "Digit1", shift: true },
|
||||
"é": { key: "Digit2" },
|
||||
2: { key: "Digit2", shift: true },
|
||||
"~": { key: "Digit2", altRight: true },
|
||||
"\"": { key: "Digit3" },
|
||||
3: { key: "Digit3", shift: true },
|
||||
"#": { key: "Digit3", altRight: true },
|
||||
"'": { key: "Digit4" },
|
||||
4: { key: "Digit4", shift: true },
|
||||
"{": { key: "Digit4", altRight: true },
|
||||
"(": { key: "Digit5" },
|
||||
5: { key: "Digit5", shift: true },
|
||||
"[": { key: "Digit5", altRight: true },
|
||||
"-": { key: "Digit6" },
|
||||
6: { key: "Digit6", shift: true },
|
||||
"|": { key: "Digit6", altRight: true },
|
||||
"è": { key: "Digit7" },
|
||||
7: { key: "Digit7", shift: true },
|
||||
"`": { key: "Digit7", altRight: true },
|
||||
"_": { key: "Digit8" },
|
||||
8: { key: "Digit8", shift: true },
|
||||
"\\": { key: "Digit8", altRight: true },
|
||||
"ç": { key: "Digit9" },
|
||||
9: { key: "Digit9", shift: true },
|
||||
"^": { key: "Digit9", altRight: true },
|
||||
"à": { key: "Digit0" },
|
||||
0: { key: "Digit0", shift: true },
|
||||
"@": { key: "Digit0", altRight: true },
|
||||
")": { key: "Minus" },
|
||||
"°": { key: "Minus", shift: true },
|
||||
"]": { key: "Minus", altRight: true },
|
||||
"=": { key: "Equal" },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"}": { key: "Equal", altRight: true },
|
||||
"$": { key: "BracketRight" },
|
||||
"£": { key: "BracketRight", shift: true },
|
||||
"¤": { key: "BracketRight", altRight: true },
|
||||
"ù": { key: "Quote" },
|
||||
"%": { key: "Quote", shift: true },
|
||||
"*": { key: "Backslash" },
|
||||
"µ": { key: "Backslash", shift: true },
|
||||
",": { key: "KeyM" },
|
||||
"?": { key: "KeyM", shift: true },
|
||||
";": { key: "Comma" },
|
||||
".": { key: "Comma", shift: true },
|
||||
":": { key: "Period" },
|
||||
"/": { key: "Period", shift: true },
|
||||
"!": { key: "Slash" },
|
||||
"§": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,113 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Italiano";
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"\\": { key: "Backquote" },
|
||||
"|": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3" },
|
||||
"£": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4" },
|
||||
"$": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"'": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"ì": { key: "Equal" },
|
||||
"^": { key: "Equal", shift: true },
|
||||
"è": { key: "BracketLeft" },
|
||||
"é": { key: "BracketLeft", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"{": { key: "BracketLeft", shift: true, altRight: true },
|
||||
"+": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", shift: true },
|
||||
"]": { key: "BracketRight", altRight: true },
|
||||
"}": { key: "BracketRight", shift: true, altRight: true },
|
||||
"ò": { key: "Semicolon" },
|
||||
"ç": { key: "Semicolon", shift: true },
|
||||
"@": { key: "Semicolon", altRight: true },
|
||||
"à": { key: "Quote" },
|
||||
"°": { key: "Quote", shift: true },
|
||||
"#": { key: "Quote", altRight: true },
|
||||
"ù": { key: "Backslash" },
|
||||
"§": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,167 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Norsk bokmål";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"ä": { key: "KeyA", accentKey: keyTrema },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"é": { key: "KeyE", accentKey: keyAcute },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "KeyO", accentKey: keyTrema },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"|": { key: "Backquote" },
|
||||
"§": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
"£": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"¤": { key: "Digit4", shift: true },
|
||||
"$": { key: "Digit4", altRight: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
"{": { key: "Digit7", altRight: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
"[": { key: "Digit8", altRight: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
"]": { key: "Digit9", altRight: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"}": { key: "Digit0", altRight: true },
|
||||
"+": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"\\": { key: "Equal" },
|
||||
"å": { key: "BracketLeft" },
|
||||
"Å": { key: "BracketLeft", shift: true },
|
||||
"ø": { key: "Semicolon" },
|
||||
"Ø": { key: "Semicolon", shift: true },
|
||||
"æ": { key: "Quote" },
|
||||
"Æ": { key: "Quote", shift: true },
|
||||
"'": { key: "Backslash" },
|
||||
"*": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,164 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Svenska";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"é": { key: "KeyE", accentKey: keyAcute },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"§": { key: "Backquote" },
|
||||
"½": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
"£": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"¤": { key: "Digit4", shift: true },
|
||||
"$": { key: "Digit4", altRight: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
"{": { key: "Digit7", altRight: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
"[": { key: "Digit8", altRight: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
"]": { key: "Digit9", altRight: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"}": { key: "Digit0", altRight: true },
|
||||
"+": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"\\": { key: "Minus", altRight: true },
|
||||
"å": { key: "BracketLeft" },
|
||||
"Å": { key: "BracketLeft", shift: true },
|
||||
"ö": { key: "Semicolon" },
|
||||
"Ö": { key: "Semicolon", shift: true },
|
||||
"ä": { key: "Quote" },
|
||||
"Ä": { key: "Quote", shift: true },
|
||||
"'": { key: "Backslash" },
|
||||
"*": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"|": { key: "IntlBackslash", altRight: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -1,6 +1,6 @@
|
|||
// Key codes and modifiers correspond to definitions in the
|
||||
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
|
||||
export const keys = {
|
||||
AltLeft: 0xe2,
|
||||
AltRight: 0xe6,
|
||||
ArrowDown: 0x51,
|
||||
ArrowLeft: 0x50,
|
||||
ArrowRight: 0x4f,
|
||||
|
@ -43,7 +43,7 @@ export const keys = {
|
|||
F13: 0x68,
|
||||
Home: 0x4a,
|
||||
Insert: 0x49,
|
||||
IntlBackslash: 0x31,
|
||||
IntlBackslash: 0x64,
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
|
@ -86,122 +86,24 @@ export const keys = {
|
|||
NumpadAdd: 0x57,
|
||||
NumpadDivide: 0x54,
|
||||
NumpadEnter: 0x58,
|
||||
NumpadEqual: 0x67,
|
||||
NumpadMultiply: 0x55,
|
||||
NumpadSubtract: 0x56,
|
||||
NumpadDecimal: 0x63,
|
||||
PageDown: 0x4e,
|
||||
PageUp: 0x4b,
|
||||
Period: 0x37,
|
||||
PrintScreen: 0x46,
|
||||
Pause: 0x48,
|
||||
Quote: 0x34,
|
||||
ScrollLock: 0x47,
|
||||
Semicolon: 0x33,
|
||||
Slash: 0x38,
|
||||
Space: 0x2c,
|
||||
SystemRequest: 0x9a,
|
||||
Tab: 0x2b,
|
||||
} as Record<string, number>;
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA", shift: false },
|
||||
b: { key: "KeyB", shift: false },
|
||||
c: { key: "KeyC", shift: false },
|
||||
d: { key: "KeyD", shift: false },
|
||||
e: { key: "KeyE", shift: false },
|
||||
f: { key: "KeyF", shift: false },
|
||||
g: { key: "KeyG", shift: false },
|
||||
h: { key: "KeyH", shift: false },
|
||||
i: { key: "KeyI", shift: false },
|
||||
j: { key: "KeyJ", shift: false },
|
||||
k: { key: "KeyK", shift: false },
|
||||
l: { key: "KeyL", shift: false },
|
||||
m: { key: "KeyM", shift: false },
|
||||
n: { key: "KeyN", shift: false },
|
||||
o: { key: "KeyO", shift: false },
|
||||
p: { key: "KeyP", shift: false },
|
||||
q: { key: "KeyQ", shift: false },
|
||||
r: { key: "KeyR", shift: false },
|
||||
s: { key: "KeyS", shift: false },
|
||||
t: { key: "KeyT", shift: false },
|
||||
u: { key: "KeyU", shift: false },
|
||||
v: { key: "KeyV", shift: false },
|
||||
w: { key: "KeyW", shift: false },
|
||||
x: { key: "KeyX", shift: false },
|
||||
y: { key: "KeyY", shift: false },
|
||||
z: { key: "KeyZ", shift: false },
|
||||
1: { key: "Digit1", shift: false },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2", shift: false },
|
||||
"@": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3", shift: false },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4", shift: false },
|
||||
$: { key: "Digit4", shift: true },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
5: { key: "Digit5", shift: false },
|
||||
"^": { key: "Digit6", shift: true },
|
||||
6: { key: "Digit6", shift: false },
|
||||
"&": { key: "Digit7", shift: true },
|
||||
7: { key: "Digit7", shift: false },
|
||||
"*": { key: "Digit8", shift: true },
|
||||
8: { key: "Digit8", shift: false },
|
||||
"(": { key: "Digit9", shift: true },
|
||||
9: { key: "Digit9", shift: false },
|
||||
")": { key: "Digit0", shift: true },
|
||||
0: { key: "Digit0", shift: false },
|
||||
"-": { key: "Minus", shift: false },
|
||||
_: { key: "Minus", shift: true },
|
||||
"=": { key: "Equal", shift: false },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"'": { key: "Quote", shift: false },
|
||||
'"': { key: "Quote", shift: true },
|
||||
",": { key: "Comma", shift: false },
|
||||
"<": { key: "Comma", shift: true },
|
||||
"/": { key: "Slash", shift: false },
|
||||
"?": { key: "Slash", shift: true },
|
||||
".": { key: "Period", shift: false },
|
||||
">": { key: "Period", shift: true },
|
||||
";": { key: "Semicolon", shift: false },
|
||||
":": { key: "Semicolon", shift: true },
|
||||
"[": { key: "BracketLeft", shift: false },
|
||||
"{": { key: "BracketLeft", shift: true },
|
||||
"]": { key: "BracketRight", shift: false },
|
||||
"}": { key: "BracketRight", shift: true },
|
||||
"\\": { key: "Backslash", shift: false },
|
||||
"|": { key: "Backslash", shift: true },
|
||||
"`": { key: "Backquote", shift: false },
|
||||
"~": { key: "Backquote", shift: true },
|
||||
"§": { key: "IntlBackslash", shift: false },
|
||||
"±": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space", shift: false },
|
||||
"\n": { key: "Enter", shift: false },
|
||||
Enter: { key: "Enter", shift: false },
|
||||
Tab: { key: "Tab", shift: false },
|
||||
} as Record<string, { key: string | number; shift: boolean }>;
|
||||
|
||||
export const modifiers = {
|
||||
ControlLeft: 0x01,
|
||||
ControlRight: 0x10,
|
||||
|
@ -227,9 +129,11 @@ export const modifierDisplayMap: Record<string, string> = {
|
|||
export const keyDisplayMap: Record<string, string> = {
|
||||
CtrlAltDelete: "Ctrl + Alt + Delete",
|
||||
AltMetaEscape: "Alt + Meta + Escape",
|
||||
CtrlAltBackspace: "Ctrl + Alt + Backspace",
|
||||
Escape: "esc",
|
||||
Tab: "tab",
|
||||
Backspace: "backspace",
|
||||
"(Backspace)": "backspace",
|
||||
Enter: "enter",
|
||||
CapsLock: "caps lock",
|
||||
ShiftLeft: "shift",
|
||||
|
@ -240,11 +144,12 @@ export const keyDisplayMap: Record<string, string> = {
|
|||
MetaLeft: "meta",
|
||||
MetaRight: "meta",
|
||||
Space: " ",
|
||||
Insert: "insert",
|
||||
Home: "home",
|
||||
PageUp: "pageup",
|
||||
PageUp: "page up",
|
||||
Delete: "delete",
|
||||
End: "end",
|
||||
PageDown: "pagedown",
|
||||
PageDown: "page down",
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
ArrowUp: "↑",
|
||||
|
@ -258,22 +163,45 @@ export const keyDisplayMap: Record<string, string> = {
|
|||
KeyU: "u", KeyV: "v", KeyW: "w", KeyX: "x", KeyY: "y",
|
||||
KeyZ: "z",
|
||||
|
||||
// Capital letters
|
||||
"(KeyA)": "A", "(KeyB)": "B", "(KeyC)": "C", "(KeyD)": "D", "(KeyE)": "E",
|
||||
"(KeyF)": "F", "(KeyG)": "G", "(KeyH)": "H", "(KeyI)": "I", "(KeyJ)": "J",
|
||||
"(KeyK)": "K", "(KeyL)": "L", "(KeyM)": "M", "(KeyN)": "N", "(KeyO)": "O",
|
||||
"(KeyP)": "P", "(KeyQ)": "Q", "(KeyR)": "R", "(KeyS)": "S", "(KeyT)": "T",
|
||||
"(KeyU)": "U", "(KeyV)": "V", "(KeyW)": "W", "(KeyX)": "X", "(KeyY)": "Y",
|
||||
"(KeyZ)": "Z",
|
||||
|
||||
// Numbers
|
||||
Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4", Digit5: "5",
|
||||
Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9", Digit0: "0",
|
||||
|
||||
// Shifted Numbers
|
||||
"(Digit1)": "!", "(Digit2)": "@", "(Digit3)": "#", "(Digit4)": "$", "(Digit5)": "%",
|
||||
"(Digit6)": "^", "(Digit7)": "&", "(Digit8)": "*", "(Digit9)": "(", "(Digit0)": ")",
|
||||
|
||||
// Symbols
|
||||
Minus: "-",
|
||||
"(Minus)": "_",
|
||||
Equal: "=",
|
||||
"(Equal)": "+",
|
||||
BracketLeft: "[",
|
||||
"(BracketLeft)": "{",
|
||||
BracketRight: "]",
|
||||
"(BracketRight)": "}",
|
||||
Backslash: "\\",
|
||||
"(Backslash)": "|",
|
||||
Semicolon: ";",
|
||||
"(Semicolon)": ":",
|
||||
Quote: "'",
|
||||
"(Quote)": "\"",
|
||||
Comma: ",",
|
||||
"(Comma)": "<",
|
||||
Period: ".",
|
||||
"(Period)": ">",
|
||||
Slash: "/",
|
||||
"(Slash)": "?",
|
||||
Backquote: "`",
|
||||
"(Backquote)": "~",
|
||||
IntlBackslash: "\\",
|
||||
|
||||
// Function keys
|
||||
|
@ -287,5 +215,11 @@ export const keyDisplayMap: Record<string, string> = {
|
|||
Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8",
|
||||
Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -",
|
||||
NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .",
|
||||
NumpadEnter: "Num Enter"
|
||||
NumpadEqual: "Num =", NumpadEnter: "Num Enter",
|
||||
NumLock: "Num Lock",
|
||||
|
||||
// Modals
|
||||
PrintScreen: "prt sc", ScrollLock: "scr lk", Pause: "pause",
|
||||
"(PrintScreen)": "sys rq", "(Pause)": "break",
|
||||
SystemRequest: "sys rq", Break: "break"
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import AdoptRoute from "@routes/adopt";
|
|||
import SignupRoute from "@routes/signup";
|
||||
import LoginRoute from "@routes/login";
|
||||
import SetupRoute from "@routes/devices.$id.setup";
|
||||
import DevicesRoute, { loader as DeviceListLoader } from "@routes/devices";
|
||||
import DevicesRoute from "@routes/devices";
|
||||
import DeviceRoute, { LocalDevice } from "@routes/devices.$id";
|
||||
import Card from "@components/Card";
|
||||
import DevicesAlreadyAdopted from "@routes/devices.already-adopted";
|
||||
|
@ -32,11 +32,12 @@ import { CLOUD_API, DEVICE_API } from "./ui.config";
|
|||
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
||||
import MountRoute from "./routes/devices.$id.mount";
|
||||
import * as SettingsRoute from "./routes/devices.$id.settings";
|
||||
import SettingsKeyboardMouseRoute from "./routes/devices.$id.settings.mouse";
|
||||
import SettingsMouseRoute from "./routes/devices.$id.settings.mouse";
|
||||
import SettingsKeyboardRoute from "./routes/devices.$id.settings.keyboard";
|
||||
import api from "./api";
|
||||
import * as SettingsIndexRoute from "./routes/devices.$id.settings._index";
|
||||
import SettingsAdvancedRoute from "./routes/devices.$id.settings.advanced";
|
||||
import * as SettingsAccessIndexRoute from "./routes/devices.$id.settings.access._index";
|
||||
import SettingsAccessIndexRoute from "./routes/devices.$id.settings.access._index";
|
||||
import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
|
||||
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
|
||||
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
|
||||
|
@ -147,7 +148,11 @@ if (isOnDevice) {
|
|||
},
|
||||
{
|
||||
path: "mouse",
|
||||
element: <SettingsKeyboardMouseRoute />,
|
||||
element: <SettingsMouseRoute />,
|
||||
},
|
||||
{
|
||||
path: "keyboard",
|
||||
element: <SettingsKeyboardRoute />,
|
||||
},
|
||||
{
|
||||
path: "advanced",
|
||||
|
@ -166,7 +171,7 @@ if (isOnDevice) {
|
|||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <SettingsAccessIndexRoute.default />,
|
||||
element: <SettingsAccessIndexRoute />,
|
||||
loader: SettingsAccessIndexRoute.loader,
|
||||
},
|
||||
{
|
||||
|
@ -276,7 +281,11 @@ if (isOnDevice) {
|
|||
},
|
||||
{
|
||||
path: "mouse",
|
||||
element: <SettingsKeyboardMouseRoute />,
|
||||
element: <SettingsMouseRoute />,
|
||||
},
|
||||
{
|
||||
path: "keyboard",
|
||||
element: <SettingsKeyboardRoute />,
|
||||
},
|
||||
{
|
||||
path: "advanced",
|
||||
|
@ -291,7 +300,7 @@ if (isOnDevice) {
|
|||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <SettingsAccessIndexRoute.default />,
|
||||
element: <SettingsAccessIndexRoute />,
|
||||
loader: SettingsAccessIndexRoute.loader,
|
||||
},
|
||||
{
|
||||
|
@ -341,7 +350,10 @@ if (isOnDevice) {
|
|||
loader: DeviceIdRename.loader,
|
||||
action: DeviceIdRename.action,
|
||||
},
|
||||
{ path: "devices", element: <DevicesRoute />, loader: DeviceListLoader },
|
||||
{
|
||||
path: "devices",
|
||||
element: <DevicesRoute />,
|
||||
loader: DevicesRoute.loader },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -356,7 +368,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
<Notifications
|
||||
toastOptions={{
|
||||
className:
|
||||
"rounded border-none bg-white text-black shadow outline outline-1 outline-slate-800/30",
|
||||
"rounded-sm border-none bg-white text-black shadow-sm outline-1 outline-slate-800/30",
|
||||
}}
|
||||
max={2}
|
||||
/>
|
||||
|
|
|
@ -71,12 +71,13 @@ export default function DevicesIdDeregister() {
|
|||
const error = useActionData() as { message: string };
|
||||
|
||||
return (
|
||||
<div className="grid min-h-screen grid-rows-layout">
|
||||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<DashboardNavbar
|
||||
isLoggedIn={!!user}
|
||||
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
|
||||
userEmail={user?.email}
|
||||
picture={user?.picture}
|
||||
kvmName={device?.name}
|
||||
/>
|
||||
|
||||
<div className="w-full h-full">
|
||||
|
|
|
@ -320,7 +320,7 @@ function ModeSelectionView({
|
|||
].map(({ label, description, value: mode, icon: Icon, tag, disabled }, index) => (
|
||||
<div
|
||||
key={label}
|
||||
className={cx("animate-fadeIn opacity-0")}
|
||||
className={cx("animate-fadeIn")}
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: `${25 * (index * 5)}ms`,
|
||||
|
@ -328,7 +328,7 @@ function ModeSelectionView({
|
|||
>
|
||||
<Card
|
||||
className={cx(
|
||||
"w-full min-w-[250px] cursor-pointer bg-white shadow-sm transition-all duration-100 hover:shadow-md dark:bg-slate-800",
|
||||
"w-full min-w-[250px] cursor-pointer bg-white shadow-xs transition-all duration-100 hover:shadow-md dark:bg-slate-800",
|
||||
{
|
||||
"ring-2 ring-blue-700": selectedMode === mode,
|
||||
"hover:ring-2 hover:ring-blue-500": selectedMode !== mode && !disabled,
|
||||
|
@ -373,7 +373,7 @@ function ModeSelectionView({
|
|||
))}
|
||||
</div>
|
||||
<div
|
||||
className="flex animate-fadeIn justify-end opacity-0"
|
||||
className="flex animate-fadeIn justify-end"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
@ -414,7 +414,7 @@ function BrowserFileView({
|
|||
if (file?.name.endsWith(".iso")) {
|
||||
setUsbMode("CDROM");
|
||||
} else if (file?.name.endsWith(".img")) {
|
||||
setUsbMode("CDROM");
|
||||
setUsbMode("Disk");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -437,7 +437,7 @@ function BrowserFileView({
|
|||
className="block cursor-pointer select-none"
|
||||
>
|
||||
<div
|
||||
className="group animate-fadeIn opacity-0"
|
||||
className="group animate-fadeIn"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
|
@ -483,7 +483,7 @@ function BrowserFileView({
|
|||
</div>
|
||||
|
||||
<div
|
||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||
className="flex w-full animate-fadeIn items-end justify-between"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -566,7 +566,7 @@ function UrlView({
|
|||
if (url.endsWith(".iso")) {
|
||||
setUsbMode("CDROM");
|
||||
} else if (url.endsWith(".img")) {
|
||||
setUsbMode("CDROM");
|
||||
setUsbMode("Disk");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -578,7 +578,7 @@ function UrlView({
|
|||
/>
|
||||
|
||||
<div
|
||||
className="animate-fadeIn opacity-0"
|
||||
className="animate-fadeIn"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
|
@ -593,7 +593,7 @@ function UrlView({
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||
className="flex w-full animate-fadeIn items-end justify-between"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -619,7 +619,7 @@ function UrlView({
|
|||
|
||||
<hr className="border-slate-800/30 dark:border-slate-300/20" />
|
||||
<div
|
||||
className="animate-fadeIn opacity-0"
|
||||
className="animate-fadeIn"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
@ -773,7 +773,7 @@ function DeviceFileView({
|
|||
if (file.name.endsWith(".iso")) {
|
||||
setUsbMode("CDROM");
|
||||
} else if (file.name.endsWith(".img")) {
|
||||
setUsbMode("CDROM");
|
||||
setUsbMode("Disk");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -797,7 +797,7 @@ function DeviceFileView({
|
|||
description="Select an image to mount from the JetKVM storage"
|
||||
/>
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
className="w-full animate-fadeIn"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -886,7 +886,7 @@ function DeviceFileView({
|
|||
|
||||
{onStorageFiles.length > 0 ? (
|
||||
<div
|
||||
className="flex animate-fadeIn items-end justify-between opacity-0"
|
||||
className="flex animate-fadeIn items-end justify-between"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.15s",
|
||||
|
@ -914,7 +914,7 @@ function DeviceFileView({
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex animate-fadeIn items-end justify-end opacity-0"
|
||||
className="flex animate-fadeIn items-end justify-end"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.15s",
|
||||
|
@ -927,7 +927,7 @@ function DeviceFileView({
|
|||
)}
|
||||
<hr className="border-slate-800/20 dark:border-slate-300/20" />
|
||||
<div
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
className="animate-fadeIn space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.20s",
|
||||
|
@ -941,9 +941,9 @@ function DeviceFileView({
|
|||
{percentageUsed}% used
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-sm bg-slate-200 dark:bg-slate-700">
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700">
|
||||
<div
|
||||
className="h-full rounded-sm bg-blue-700 transition-all duration-300 ease-in-out dark:bg-blue-500"
|
||||
className="h-full rounded-xs bg-blue-700 transition-all duration-300 ease-in-out dark:bg-blue-500"
|
||||
style={{ width: `${percentageUsed}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
@ -959,7 +959,7 @@ function DeviceFileView({
|
|||
|
||||
{onStorageFiles.length > 0 && (
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
className="w-full animate-fadeIn"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.25s",
|
||||
|
@ -1251,7 +1251,7 @@ function UploadFileView({
|
|||
}
|
||||
/>
|
||||
<div
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
className="animate-fadeIn space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
|
@ -1365,7 +1365,7 @@ function UploadFileView({
|
|||
{/* Display upload error if present */}
|
||||
{uploadError && (
|
||||
<div
|
||||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 opacity-0 dark:text-red-400"
|
||||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400"
|
||||
style={{ animationDuration: "0.7s" }}
|
||||
>
|
||||
Error: {uploadError}
|
||||
|
@ -1373,7 +1373,7 @@ function UploadFileView({
|
|||
)}
|
||||
|
||||
<div
|
||||
className="flex w-full animate-fadeIn items-end opacity-0"
|
||||
className="flex w-full animate-fadeIn items-end"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -1496,7 +1496,7 @@ function PreUploadedImageItem({
|
|||
</div>
|
||||
<div className="relative flex select-none items-center gap-x-3">
|
||||
<div
|
||||
className={cx("opacity-0 transition-opacity duration-200", {
|
||||
className={cx("opacity-0 transition-opacity duration-200", {
|
||||
"w-auto opacity-100": isHovering,
|
||||
})}
|
||||
>
|
||||
|
@ -1579,7 +1579,6 @@ function UsbModeSelector({
|
|||
type="radio"
|
||||
id="disk"
|
||||
name="mountType"
|
||||
disabled
|
||||
checked={usbMode === "Disk"}
|
||||
onChange={() => setUsbMode("Disk")}
|
||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
|
@ -1588,9 +1587,6 @@ function UsbModeSelector({
|
|||
<span className="text-sm font-medium leading-none text-slate-900 opacity-50 dark:text-white">
|
||||
Disk
|
||||
</span>
|
||||
<div className="text-[10px] text-slate-500 dark:text-slate-400">
|
||||
Coming soon
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -75,12 +75,13 @@ export default function DeviceIdRename() {
|
|||
const error = useActionData() as { message: string };
|
||||
|
||||
return (
|
||||
<div className="grid min-h-screen grid-rows-layout">
|
||||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<DashboardNavbar
|
||||
isLoggedIn={!!user}
|
||||
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
|
||||
userEmail={user?.email}
|
||||
picture={user?.picture}
|
||||
kvmName={device?.name}
|
||||
/>
|
||||
|
||||
<div className="h-full w-full">
|
||||
|
|
|
@ -26,7 +26,7 @@ export interface TLSState {
|
|||
privateKey?: string;
|
||||
}
|
||||
|
||||
export const loader = async () => {
|
||||
const loader = async () => {
|
||||
if (isOnDevice) {
|
||||
const status = await api
|
||||
.GET(`${DEVICE_API}/device`)
|
||||
|
@ -468,3 +468,5 @@ export default function SettingsAccessIndexRoute() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsAccessIndexRoute.loader = loader;
|
|
@ -15,6 +15,25 @@ export default function SettingsHardwareRoute() {
|
|||
const [send] = useJsonRpc();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
const setDisplayRotation = useSettingsStore(state => state.setDisplayRotation);
|
||||
|
||||
const handleDisplayRotationChange = (rotation: string) => {
|
||||
setDisplayRotation(rotation);
|
||||
handleDisplayRotationSave();
|
||||
};
|
||||
|
||||
const handleDisplayRotationSave = () => {
|
||||
send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Display orientation updated successfully");
|
||||
});
|
||||
};
|
||||
|
||||
const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings);
|
||||
|
||||
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
||||
|
@ -59,6 +78,24 @@ export default function SettingsHardwareRoute() {
|
|||
description="Configure display settings and hardware options for your JetKVM device"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Display Orientation"
|
||||
description="Set the orientation of the display"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.displayRotation.toString()}
|
||||
options={[
|
||||
{ value: "270", label: "Normal" },
|
||||
{ value: "90", label: "Inverted" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.displayRotation = e.target.value;
|
||||
handleDisplayRotationChange(settings.displayRotation);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Display Brightness"
|
||||
description="Set the brightness of the display"
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { useDeviceSettingsStore } from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { layouts } from "@/keyboardLayouts";
|
||||
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
export default function SettingsKeyboardRoute() {
|
||||
const keyboardLayout = useDeviceSettingsStore(state => state.keyboardLayout);
|
||||
const setKeyboardLayout = useDeviceSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
|
||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
useEffect(() => {
|
||||
send("getKeyboardLayout", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onKeyboardLayoutChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const layout = e.target.value;
|
||||
send("setKeyboardLayout", { layout }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
notifications.success("Keyboard layout set successfully");
|
||||
setKeyboardLayout(layout);
|
||||
});
|
||||
},
|
||||
[send, setKeyboardLayout],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Keyboard"
|
||||
description="Configure keyboard layout settings for your device"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<FeatureFlag minAppVersion="0.4.0" name="Paste text">
|
||||
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
||||
<SettingsItem
|
||||
title="Paste text"
|
||||
description="Keyboard layout of target operating system"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
fullWidth
|
||||
value={keyboardLayout}
|
||||
onChange={onKeyboardLayoutChange}
|
||||
options={layoutOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Pasting text sends 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.
|
||||
</p>
|
||||
</FeatureFlag>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,15 @@
|
|||
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand } from "react-icons/lu";
|
||||
import {
|
||||
LuPenLine,
|
||||
LuCopy,
|
||||
LuMoveRight,
|
||||
LuCornerDownRight,
|
||||
LuArrowUp,
|
||||
LuArrowDown,
|
||||
LuTrash2,
|
||||
LuCommand,
|
||||
} from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
|
@ -26,10 +35,10 @@ export default function SettingsMacrosRoute() {
|
|||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
|
||||
|
||||
const isMaxMacrosReached = useMemo(() =>
|
||||
macros.length >= MAX_TOTAL_MACROS,
|
||||
[macros.length]
|
||||
|
||||
const isMaxMacrosReached = useMemo(
|
||||
() => macros.length >= MAX_TOTAL_MACROS,
|
||||
[macros.length],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -38,75 +47,83 @@ export default function SettingsMacrosRoute() {
|
|||
}
|
||||
}, [initialized, loadMacros]);
|
||||
|
||||
const handleDuplicateMacro = useCallback(async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaxMacrosReached) {
|
||||
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionLoadingId(macro.id);
|
||||
|
||||
const newMacroCopy: KeySequence = {
|
||||
...JSON.parse(JSON.stringify(macro)),
|
||||
id: generateMacroId(),
|
||||
name: `${macro.name} ${COPY_SUFFIX}`,
|
||||
sortOrder: macros.length + 1,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveMacros(normalizeSortOrders([...macros, newMacroCopy]));
|
||||
notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to duplicate macro: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to duplicate macro");
|
||||
const handleDuplicateMacro = useCallback(
|
||||
async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
}, [isMaxMacrosReached, macros, saveMacros, setActionLoadingId]);
|
||||
|
||||
const handleMoveMacro = useCallback(async (index: number, direction: 'up' | 'down', macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= macros.length) return;
|
||||
|
||||
setActionLoadingId(macroId);
|
||||
|
||||
try {
|
||||
const newMacros = [...macros];
|
||||
[newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]];
|
||||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
if (isMaxMacrosReached) {
|
||||
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
}, [macros, saveMacros, setActionLoadingId]);
|
||||
|
||||
setActionLoadingId(macro.id);
|
||||
|
||||
const newMacroCopy: KeySequence = {
|
||||
...JSON.parse(JSON.stringify(macro)),
|
||||
id: generateMacroId(),
|
||||
name: `${macro.name} ${COPY_SUFFIX}`,
|
||||
sortOrder: macros.length + 1,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveMacros(normalizeSortOrders([...macros, newMacroCopy]));
|
||||
notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to duplicate macro: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to duplicate macro");
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
},
|
||||
[isMaxMacrosReached, macros, saveMacros, setActionLoadingId],
|
||||
);
|
||||
|
||||
const handleMoveMacro = useCallback(
|
||||
async (index: number, direction: "up" | "down", macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= macros.length) return;
|
||||
|
||||
setActionLoadingId(macroId);
|
||||
|
||||
try {
|
||||
const newMacros = [...macros];
|
||||
[newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]];
|
||||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
},
|
||||
[macros, saveMacros, setActionLoadingId],
|
||||
);
|
||||
|
||||
const handleDeleteMacro = useCallback(async () => {
|
||||
if (!macroToDelete?.id) return;
|
||||
|
||||
setActionLoadingId(macroToDelete.id);
|
||||
try {
|
||||
const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macroToDelete.id));
|
||||
const updatedMacros = normalizeSortOrders(
|
||||
macros.filter(m => m.id !== macroToDelete.id),
|
||||
);
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success(`Macro "${macroToDelete.name}" deleted successfully`);
|
||||
setShowDeleteConfirm(false);
|
||||
|
@ -122,135 +139,168 @@ export default function SettingsMacrosRoute() {
|
|||
}
|
||||
}, [macroToDelete, macros, saveMacros]);
|
||||
|
||||
const MacroList = useMemo(() => (
|
||||
<div className="space-y-2">
|
||||
{macros.map((macro, index) => (
|
||||
<Card key={macro.id} className="p-2 bg-white dark:bg-slate-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'up', macro.id)}
|
||||
disabled={index === 0 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowUp}
|
||||
aria-label={`Move ${macro.name} up`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'down', macro.id)}
|
||||
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowDown}
|
||||
aria-label={`Move ${macro.name} down`}
|
||||
/>
|
||||
</div>
|
||||
const MacroList = useMemo(
|
||||
() => (
|
||||
<div className="space-y-2">
|
||||
{macros.map((macro, index) => (
|
||||
<Card key={macro.id} className="bg-white p-2 dark:bg-slate-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, "up", macro.id)}
|
||||
disabled={index === 0 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowUp}
|
||||
aria-label={`Move ${macro.name} up`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, "down", macro.id)}
|
||||
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowDown}
|
||||
aria-label={`Move ${macro.name} down`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center ml-2">
|
||||
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
|
||||
{macro.name}
|
||||
</h3>
|
||||
<p className="mt-1 ml-4 text-xs text-slate-500 dark:text-slate-400 overflow-hidden">
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
{macro.steps.map((step, stepIndex) => {
|
||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||
<div className="ml-2 flex min-w-0 flex-1 flex-col justify-center">
|
||||
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
|
||||
{macro.name}
|
||||
</h3>
|
||||
<p className="mt-1 ml-4 overflow-hidden text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
{macro.steps.map((step, stepIndex) => {
|
||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||
|
||||
return (
|
||||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 text-slate-400 dark:text-slate-500 h-3 w-3 flex-shrink-0" />
|
||||
<span className="bg-slate-50 dark:bg-slate-800 px-2 py-0.5 rounded-md border border-slate-200/50 dark:border-slate-700/50">
|
||||
{(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => (
|
||||
<Fragment key={`mod-${idx}`}>
|
||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||
{modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
return (
|
||||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 h-3 w-3 shrink-0 text-slate-400 dark:text-slate-500" />
|
||||
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
|
||||
{(Array.isArray(step.modifiers) &&
|
||||
step.modifiers.length > 0) ||
|
||||
(Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) &&
|
||||
step.modifiers.map((modifier, idx) => (
|
||||
<Fragment key={`mod-${idx}`}>
|
||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||
{modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{Array.isArray(step.modifiers) &&
|
||||
step.modifiers.length > 0 &&
|
||||
Array.isArray(step.keys) &&
|
||||
step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
|
||||
{Array.isArray(step.keys) && step.keys.map((key, idx) => (
|
||||
<Fragment key={`key-${idx}`}>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||
{keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">Delay only</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">({step.delay}ms)</span>
|
||||
)}
|
||||
{Array.isArray(step.keys) &&
|
||||
step.keys.map((key, idx) => (
|
||||
<Fragment key={`key-${idx}`}>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||
{keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">
|
||||
Delay only
|
||||
</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">
|
||||
({step.delay}ms)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
<Button
|
||||
size="XS"
|
||||
className="text-red-500 dark:text-red-400"
|
||||
theme="light"
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={() => {
|
||||
setMacroToDelete(macro);
|
||||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Delete macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuCopy}
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
text="Edit"
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Edit macro ${macro.name}`}
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-1">
|
||||
<Button
|
||||
size="XS"
|
||||
className="text-red-500 dark:text-red-400"
|
||||
theme="light"
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={() => {
|
||||
setMacroToDelete(macro);
|
||||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Delete macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuCopy}
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
text="Edit"
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Edit macro ${macro.name}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<ConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onClose={() => {
|
||||
setShowDeleteConfirm(false);
|
||||
setMacroToDelete(null);
|
||||
}}
|
||||
title="Delete Macro"
|
||||
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
|
||||
variant="danger"
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
|
||||
onConfirm={handleDeleteMacro}
|
||||
isConfirming={actionLoadingId === macroToDelete?.id}
|
||||
/>
|
||||
</div>
|
||||
), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]);
|
||||
<ConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onClose={() => {
|
||||
setShowDeleteConfirm(false);
|
||||
setMacroToDelete(null);
|
||||
}}
|
||||
title="Delete Macro"
|
||||
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
|
||||
variant="danger"
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
|
||||
onConfirm={handleDeleteMacro}
|
||||
isConfirming={actionLoadingId === macroToDelete?.id}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[
|
||||
macros,
|
||||
showDeleteConfirm,
|
||||
macroToDelete?.name,
|
||||
macroToDelete?.id,
|
||||
actionLoadingId,
|
||||
handleDeleteMacro,
|
||||
handleMoveMacro,
|
||||
handleDuplicateMacro,
|
||||
navigate,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
@ -259,7 +309,7 @@ export default function SettingsMacrosRoute() {
|
|||
title="Keyboard Macros"
|
||||
description={`Combine keystrokes into a single action for faster workflows.`}
|
||||
/>
|
||||
{ macros.length > 0 && (
|
||||
{macros.length > 0 && (
|
||||
<div className="flex items-center pl-2">
|
||||
<Button
|
||||
size="SM"
|
||||
|
@ -288,6 +338,7 @@ export default function SettingsMacrosRoute() {
|
|||
<EmptyCard
|
||||
IconElm={LuCommand}
|
||||
headline="Create Your First Macro"
|
||||
description="Combine keystrokes into a single action"
|
||||
BtnElm={
|
||||
<Button
|
||||
size="SM"
|
||||
|
@ -299,7 +350,9 @@ export default function SettingsMacrosRoute() {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
) : MacroList}
|
||||
) : (
|
||||
MacroList
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ import { SettingsItem } from "./devices.$id.settings";
|
|||
|
||||
type ScrollSensitivity = "low" | "default" | "high";
|
||||
|
||||
export default function SettingsKeyboardMouseRoute() {
|
||||
export default function SettingsMouseRoute() {
|
||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
||||
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
|
||||
import { IPv4Mode, IPv6Mode, LLDPMode, mDNSMode, NetworkSettings, NetworkState, TimeSyncMode, useNetworkStateStore } from "@/hooks/stores";
|
||||
import {
|
||||
IPv4Mode,
|
||||
IPv6Mode,
|
||||
LLDPMode,
|
||||
mDNSMode,
|
||||
NetworkSettings,
|
||||
NetworkState,
|
||||
TimeSyncMode,
|
||||
useNetworkStateStore,
|
||||
} from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import InputField from "@components/InputField";
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
@ -25,13 +36,9 @@ const defaultNetworkSettings: NetworkSettings = {
|
|||
lldp_tx_tlvs: [],
|
||||
mdns_mode: "unknown",
|
||||
time_sync_mode: "unknown",
|
||||
}
|
||||
};
|
||||
|
||||
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||
if (lifetime == "") {
|
||||
return <strong>N/A</strong>;
|
||||
}
|
||||
|
||||
const [remaining, setRemaining] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -43,46 +50,91 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
|||
return () => clearInterval(interval);
|
||||
}, [lifetime]);
|
||||
|
||||
return <>
|
||||
<strong>{dayjs(lifetime).format()}</strong>
|
||||
{remaining && <>
|
||||
{" "}<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
({remaining})
|
||||
</span>
|
||||
</>}
|
||||
</>
|
||||
if (lifetime == "") {
|
||||
return <strong>N/A</strong>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<strong>{dayjs(lifetime).format("YYYY-MM-DD HH:mm")}</strong>
|
||||
{remaining && (
|
||||
<>
|
||||
{" "}
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
({remaining})
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsNetworkRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [state, state.setNetworkState]);
|
||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
||||
state,
|
||||
state.setNetworkState,
|
||||
]);
|
||||
|
||||
const [networkSettings, setNetworkSettings] =
|
||||
useState<NetworkSettings>(defaultNetworkSettings);
|
||||
|
||||
// We use this to determine whether the settings have changed
|
||||
const firstNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
|
||||
|
||||
const [networkSettings, setNetworkSettings] = useState<NetworkSettings>(defaultNetworkSettings);
|
||||
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
|
||||
|
||||
const [customDomain, setCustomDomain] = useState<string>("");
|
||||
const [selectedDomainOption, setSelectedDomainOption] = useState<string>("dhcp");
|
||||
|
||||
useEffect(() => {
|
||||
if (networkSettings.domain && networkSettingsLoaded) {
|
||||
// Check if the domain is one of the predefined options
|
||||
const predefinedOptions = ["dhcp", "local"];
|
||||
if (predefinedOptions.includes(networkSettings.domain)) {
|
||||
setSelectedDomainOption(networkSettings.domain);
|
||||
} else {
|
||||
setSelectedDomainOption("custom");
|
||||
setCustomDomain(networkSettings.domain);
|
||||
}
|
||||
}
|
||||
}, [networkSettings.domain, networkSettingsLoaded]);
|
||||
|
||||
const getNetworkSettings = useCallback(() => {
|
||||
setNetworkSettingsLoaded(false);
|
||||
send("getNetworkSettings", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
console.log(resp.result);
|
||||
setNetworkSettings(resp.result as NetworkSettings);
|
||||
|
||||
if (!firstNetworkSettings.current) {
|
||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
||||
}
|
||||
setNetworkSettingsLoaded(true);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const setNetworkSettingsRemote = useCallback((settings: NetworkSettings) => {
|
||||
setNetworkSettingsLoaded(false);
|
||||
send("setNetworkSettings", { settings }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error("Failed to save network settings: " + (resp.error.data ? resp.error.data : resp.error.message));
|
||||
const setNetworkSettingsRemote = useCallback(
|
||||
(settings: NetworkSettings) => {
|
||||
setNetworkSettingsLoaded(false);
|
||||
send("setNetworkSettings", { settings }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
"Failed to save network settings: " +
|
||||
(resp.error.data ? resp.error.data : resp.error.message),
|
||||
);
|
||||
setNetworkSettingsLoaded(true);
|
||||
return;
|
||||
}
|
||||
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
||||
setNetworkSettings(resp.result as NetworkSettings);
|
||||
setNetworkSettingsLoaded(true);
|
||||
return;
|
||||
}
|
||||
setNetworkSettings(resp.result as NetworkSettings);
|
||||
setNetworkSettingsLoaded(true);
|
||||
notifications.success("Network settings saved");
|
||||
});
|
||||
}, [send]);
|
||||
notifications.success("Network settings saved");
|
||||
});
|
||||
},
|
||||
[send],
|
||||
);
|
||||
|
||||
const getNetworkState = useCallback(() => {
|
||||
send("getNetworkState", {}, resp => {
|
||||
|
@ -90,7 +142,7 @@ export default function SettingsNetworkRoute() {
|
|||
console.log(resp.result);
|
||||
setNetworkState(resp.result as NetworkState);
|
||||
});
|
||||
}, [send]);
|
||||
}, [send, setNetworkState]);
|
||||
|
||||
const handleRenewLease = useCallback(() => {
|
||||
send("renewDHCPLease", {}, resp => {
|
||||
|
@ -131,278 +183,520 @@ export default function SettingsNetworkRoute() {
|
|||
setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode });
|
||||
};
|
||||
|
||||
const filterUnknown = useCallback((options: { value: string; label: string; }[]) => {
|
||||
if (!networkSettingsLoaded) return options;
|
||||
return options.filter(option => option.value !== "unknown");
|
||||
}, [networkSettingsLoaded]);
|
||||
const handleHostnameChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, hostname: value });
|
||||
};
|
||||
|
||||
const handleDomainChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, domain: value });
|
||||
};
|
||||
|
||||
const handleDomainOptionChange = (value: string) => {
|
||||
setSelectedDomainOption(value);
|
||||
if (value !== "custom") {
|
||||
handleDomainChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomDomainChange = (value: string) => {
|
||||
setCustomDomain(value);
|
||||
handleDomainChange(value);
|
||||
};
|
||||
|
||||
const filterUnknown = useCallback(
|
||||
(options: { value: string; label: string }[]) => {
|
||||
if (!networkSettingsLoaded) return options;
|
||||
return options.filter(option => option.value !== "unknown");
|
||||
},
|
||||
[networkSettingsLoaded],
|
||||
);
|
||||
|
||||
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Network"
|
||||
description="Configure your network settings"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="MAC Address"
|
||||
description={<></>}
|
||||
>
|
||||
<span className="select-auto font-mono text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.mac_address}
|
||||
</span>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Hostname"
|
||||
description={
|
||||
<>
|
||||
Hostname for the device
|
||||
<br />
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
Leave blank for default
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputField
|
||||
type="text"
|
||||
placeholder="jetkvm"
|
||||
value={networkSettings.hostname}
|
||||
error={""}
|
||||
onChange={e => {
|
||||
setNetworkSettings({ ...networkSettings, hostname: e.target.value });
|
||||
}}
|
||||
disabled={!networkSettingsLoaded}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Domain"
|
||||
description={
|
||||
<>
|
||||
Domain for the device
|
||||
<br />
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
Leave blank to use DHCP provided domain, if there is no domain, use <span className="font-mono">local</span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputField
|
||||
type="text"
|
||||
placeholder="local"
|
||||
value={networkSettings.domain}
|
||||
error={""}
|
||||
onChange={e => {
|
||||
setNetworkSettings({ ...networkSettings, domain: e.target.value });
|
||||
}}
|
||||
disabled={!networkSettingsLoaded}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="IPv4 Mode"
|
||||
description="Configure the IPv4 mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv4_mode}
|
||||
onChange={e => handleIpv4ModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "dhcp", label: "DHCP" },
|
||||
// { value: "static", label: "Static" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.dhcp_lease && (
|
||||
<GridCard>
|
||||
<div className="flex items-start gap-x-4 p-4">
|
||||
<div className="space-y-3 w-full">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
Current DHCP Lease
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-none space-y-1 text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.dhcp_lease?.ip && <li>IP: <strong>{networkState?.dhcp_lease?.ip}</strong></li>}
|
||||
{networkState?.dhcp_lease?.netmask && <li>Subnet: <strong>{networkState?.dhcp_lease?.netmask}</strong></li>}
|
||||
{networkState?.dhcp_lease?.broadcast && <li>Broadcast: <strong>{networkState?.dhcp_lease?.broadcast}</strong></li>}
|
||||
{networkState?.dhcp_lease?.ttl && <li>TTL: <strong>{networkState?.dhcp_lease?.ttl}</strong></li>}
|
||||
{networkState?.dhcp_lease?.mtu && <li>MTU: <strong>{networkState?.dhcp_lease?.mtu}</strong></li>}
|
||||
{networkState?.dhcp_lease?.hostname && <li>Hostname: <strong>{networkState?.dhcp_lease?.hostname}</strong></li>}
|
||||
{networkState?.dhcp_lease?.domain && <li>Domain: <strong>{networkState?.dhcp_lease?.domain}</strong></li>}
|
||||
{networkState?.dhcp_lease?.routers && <li>Gateway: <strong>{networkState?.dhcp_lease?.routers.join(", ")}</strong></li>}
|
||||
{networkState?.dhcp_lease?.dns && <li>DNS: <strong>{networkState?.dhcp_lease?.dns.join(", ")}</strong></li>}
|
||||
{networkState?.dhcp_lease?.ntp_servers && <li>NTP Servers: <strong>{networkState?.dhcp_lease?.ntp_servers.join(", ")}</strong></li>}
|
||||
{networkState?.dhcp_lease?.server_id && <li>Server ID: <strong>{networkState?.dhcp_lease?.server_id}</strong></li>}
|
||||
{networkState?.dhcp_lease?.bootp_next_server && <li>BootP Next Server: <strong>{networkState?.dhcp_lease?.bootp_next_server}</strong></li>}
|
||||
{networkState?.dhcp_lease?.bootp_server_name && <li>BootP Server Name: <strong>{networkState?.dhcp_lease?.bootp_server_name}</strong></li>}
|
||||
{networkState?.dhcp_lease?.bootp_file && <li>Boot File: <strong>{networkState?.dhcp_lease?.bootp_file}</strong></li>}
|
||||
{networkState?.dhcp_lease?.lease_expiry && <li>
|
||||
Lease Expiry: <LifeTimeLabel lifetime={`${networkState?.dhcp_lease?.lease_expiry}`} />
|
||||
</li>}
|
||||
{/* {JSON.stringify(networkState?.dhcp_lease)} */}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="block w-full dark:border-slate-600" />
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Renew lease"
|
||||
onClick={() => {
|
||||
handleRenewLease();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<Fieldset disabled={!networkSettingsLoaded} className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Network"
|
||||
description="Configure your network settings"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="MAC Address"
|
||||
description="Hardware identifier for the network interface"
|
||||
>
|
||||
<InputField
|
||||
type="text"
|
||||
size="SM"
|
||||
value={networkState?.mac_address}
|
||||
error={""}
|
||||
disabled={true}
|
||||
readOnly={true}
|
||||
className="dark:!text-opacity-60"
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Hostname"
|
||||
description="Device identifier on the network. Blank for system default"
|
||||
>
|
||||
<div className="relative">
|
||||
<div>
|
||||
<InputField
|
||||
size="SM"
|
||||
type="text"
|
||||
placeholder="jetkvm"
|
||||
defaultValue={networkSettings.hostname}
|
||||
onChange={e => {
|
||||
handleHostnameChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="IPv6 Mode"
|
||||
description="Configure the IPv6 mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Domain"
|
||||
description="Network domain suffix for the device"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={selectedDomainOption}
|
||||
onChange={e => handleDomainOptionChange(e.target.value)}
|
||||
options={[
|
||||
{ value: "dhcp", label: "DHCP provided" },
|
||||
{ value: "local", label: ".local" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
{selectedDomainOption === "custom" && (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<InputField
|
||||
size="SM"
|
||||
type="text"
|
||||
placeholder="home"
|
||||
value={customDomain}
|
||||
onChange={e => setCustomDomain(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Save Domain"
|
||||
onClick={() => handleCustomDomainChange(customDomain)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="mDNS"
|
||||
description="Control mDNS (multicast DNS) operational mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.mdns_mode}
|
||||
onChange={e => handleMdnsModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "auto", label: "Auto" },
|
||||
{ value: "ipv4_only", label: "IPv4 only" },
|
||||
{ value: "ipv6_only", label: "IPv6 only" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Time synchronization"
|
||||
description="Configure time synchronization settings"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.time_sync_mode}
|
||||
onChange={e => {
|
||||
handleTimeSyncModeChange(e.target.value);
|
||||
}}
|
||||
options={filterUnknown([
|
||||
{ value: "unknown", label: "..." },
|
||||
// { value: "auto", label: "Auto" },
|
||||
{ value: "ntp_only", label: "NTP only" },
|
||||
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
||||
{ value: "http_only", label: "HTTP only" },
|
||||
// { value: "custom", label: "Custom" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="SM"
|
||||
value={networkSettings.ipv6_mode}
|
||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
// { value: "disabled", label: "Disabled" },
|
||||
{ value: "slaac", label: "SLAAC" },
|
||||
// { value: "dhcpv6", label: "DHCPv6" },
|
||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
||||
// { value: "static", label: "Static" },
|
||||
// { value: "link_local", label: "Link-local only" },
|
||||
])}
|
||||
theme="primary"
|
||||
disabled={firstNetworkSettings.current === networkSettings}
|
||||
text="Save Settings"
|
||||
onClick={() => setNetworkSettingsRemote(networkSettings)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.ipv6_addresses && (
|
||||
<GridCard>
|
||||
<div className="flex items-start gap-x-4 p-4">
|
||||
<div className="space-y-3 w-full">
|
||||
<div className="space-y-2">
|
||||
</div>
|
||||
|
||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv4_mode}
|
||||
onChange={e => handleIpv4ModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
{ value: "dhcp", label: "DHCP" },
|
||||
// { value: "static", label: "Static" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.dhcp_lease && (
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
DHCP Lease
|
||||
</h3>
|
||||
|
||||
<div className="flex gap-x-6 gap-y-2">
|
||||
<div className="flex-1 space-y-2">
|
||||
{networkState?.dhcp_lease?.ip && (
|
||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
IP Address
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ip}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.netmask && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Subnet Mask
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.netmask}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.dns && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
DNS Servers
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.dns.map(dns => (
|
||||
<div key={dns}>{dns}</div>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.broadcast && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Broadcast
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.broadcast}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.domain && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Domain
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.domain}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.ntp_servers &&
|
||||
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
|
||||
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
||||
NTP Servers
|
||||
</div>
|
||||
<div className="shrink text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
||||
<div key={server}>{server}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.hostname && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Hostname
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.hostname}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-2">
|
||||
{networkState?.dhcp_lease?.routers &&
|
||||
networkState?.dhcp_lease?.routers.length > 0 && (
|
||||
<div className="flex justify-between pt-2">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Gateway
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.routers.map(router => (
|
||||
<div key={router}>{router}</div>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.server_id && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
DHCP Server
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.server_id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.lease_expiry && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Lease Expires
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
<LifeTimeLabel
|
||||
lifetime={`${networkState?.dhcp_lease?.lease_expiry}`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.mtu && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
MTU
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.mtu}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.ttl && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
TTL
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ttl}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.bootp_next_server && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Boot Next Server
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_next_server}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.bootp_server_name && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Boot Server Name
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_server_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.bootp_file && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Boot File
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_file}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
className="text-red-500"
|
||||
text="Renew DHCP Lease"
|
||||
LeadingIcon={ArrowPathIcon}
|
||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv6_mode}
|
||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
// { value: "disabled", label: "Disabled" },
|
||||
{ value: "slaac", label: "SLAAC" },
|
||||
// { value: "dhcpv6", label: "DHCPv6" },
|
||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
||||
// { value: "static", label: "Static" },
|
||||
// { value: "link_local", label: "Link-local only" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.ipv6_addresses && (
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Information
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Link-local
|
||||
</h4>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.ipv6_link_local}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Addresses
|
||||
</h4>
|
||||
<ul className="list-none space-y-1 text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.map(addr => (
|
||||
<li key={addr.address}>
|
||||
{addr.address}
|
||||
{addr.valid_lifetime && <>
|
||||
<br />
|
||||
- valid_lft: {" "}
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||
</span>
|
||||
</>}
|
||||
{addr.preferred_lifetime && <>
|
||||
<br />
|
||||
- pref_lft: {" "}
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||
</span>
|
||||
</>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||
{networkState?.dhcp_lease?.ip && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Link-local
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.ipv6_link_local}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
{networkState?.ipv6_addresses &&
|
||||
networkState?.ipv6_addresses.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold">IPv6 Addresses</h4>
|
||||
{networkState.ipv6_addresses.map(addr => (
|
||||
<div
|
||||
key={addr.address}
|
||||
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-slate-100/40 p-4 pl-4 dark:border-blue-500 dark:bg-slate-900"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<div className="col-span-2 flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Address
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.address}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{addr.valid_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Valid Lifetime
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.valid_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel
|
||||
lifetime={`${addr.valid_lifetime}`}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{addr.preferred_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Preferred Lifetime
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.preferred_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel
|
||||
lifetime={`${addr.preferred_lifetime}`}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4 hidden">
|
||||
<SettingsItem
|
||||
title="LLDP"
|
||||
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.lldp_mode}
|
||||
onChange={e => handleLldpModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "basic", label: "Basic" },
|
||||
{ value: "all", label: "All" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="mDNS"
|
||||
description="Control mDNS (multicast DNS) operational mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.mdns_mode}
|
||||
onChange={e => handleMdnsModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "auto", label: "Auto" },
|
||||
{ value: "ipv4_only", label: "IPv4 only" },
|
||||
{ value: "ipv6_only", label: "IPv6 only" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Time synchronization"
|
||||
description="Configure time synchronization settings"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.time_sync_mode}
|
||||
onChange={e => handleTimeSyncModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "unknown", label: "..." },
|
||||
// { value: "auto", label: "Auto" },
|
||||
{ value: "ntp_only", label: "NTP only" },
|
||||
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
||||
{ value: "http_only", label: "HTTP only" },
|
||||
// { value: "custom", label: "Custom" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="flex items-end gap-x-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setNetworkSettingsRemote(networkSettings);
|
||||
}}
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Save Settings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden space-y-4">
|
||||
<SettingsItem
|
||||
title="LLDP"
|
||||
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.lldp_mode}
|
||||
onChange={e => handleLldpModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "basic", label: "Basic" },
|
||||
{ value: "all", label: "All" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</Fieldset>
|
||||
<ConfirmDialog
|
||||
open={showRenewLeaseConfirm}
|
||||
onClose={() => setShowRenewLeaseConfirm(false)}
|
||||
title="Renew DHCP Lease"
|
||||
description="This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process."
|
||||
variant="danger"
|
||||
confirmText="Renew Lease"
|
||||
onConfirm={() => {
|
||||
handleRenewLease();
|
||||
setShowRenewLeaseConfirm(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
import {
|
||||
LuSettings,
|
||||
LuMouse,
|
||||
LuKeyboard,
|
||||
LuVideo,
|
||||
LuCpu,
|
||||
|
@ -12,15 +13,16 @@ import {
|
|||
LuNetwork,
|
||||
} from "react-icons/lu";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import { LinkButton } from "@/components/Button";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useUiStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
|
||||
import { LinkButton } from "../components/Button";
|
||||
import { cx } from "../cva.config";
|
||||
import { useUiStore } from "../hooks/stores";
|
||||
import useKeyboard from "../hooks/useKeyboard";
|
||||
import { useResizeObserver } from "../hooks/useResizeObserver";
|
||||
import LoadingSpinner from "../components/LoadingSpinner";
|
||||
|
||||
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
export default function SettingsRoute() {
|
||||
|
@ -30,7 +32,7 @@ export default function SettingsRoute() {
|
|||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
||||
const [showRightGradient, setShowRightGradient] = useState(false);
|
||||
const { width } = useResizeObserver({ ref: scrollContainerRef });
|
||||
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
|
||||
|
||||
// Handle scroll position to show/hide gradients
|
||||
const handleScroll = () => {
|
||||
|
@ -148,11 +150,22 @@ export default function SettingsRoute() {
|
|||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<LuMouse className="h-4 w-4 shrink-0" />
|
||||
<h1>Mouse</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
to="keyboard"
|
||||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<h1>Keyboard</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
to="video"
|
||||
|
|
|
@ -56,7 +56,7 @@ export default function SetupRoute() {
|
|||
return (
|
||||
<>
|
||||
<GridBackground />
|
||||
<div className="grid min-h-screen grid-rows-layout">
|
||||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<SimpleNavbar />
|
||||
<Container>
|
||||
<div className="flex items-center justify-center w-full h-full isolate">
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
useSearchParams,
|
||||
} from "react-router-dom";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
import FocusTrap from "focus-trap-react";
|
||||
import { FocusTrap } from "focus-trap-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
|
||||
|
@ -795,7 +795,7 @@ export default function KvmIdRoute() {
|
|||
</div>
|
||||
</FocusTrap>
|
||||
|
||||
<div className="grid h-full select-none grid-rows-headerBody">
|
||||
<div className="grid h-full grid-rows-(--grid-headerBody) select-none">
|
||||
<DashboardNavbar
|
||||
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
|
||||
showConnectionStatus={true}
|
||||
|
@ -809,7 +809,7 @@ export default function KvmIdRoute() {
|
|||
<WebRTCVideo />
|
||||
<div
|
||||
style={{ animationDuration: "500ms" }}
|
||||
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4 opacity-0"
|
||||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||
>
|
||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||
|
|
|
@ -8,7 +8,7 @@ export default function DevicesAlreadyAdopted() {
|
|||
<>
|
||||
<GridBackground />
|
||||
|
||||
<div className="grid min-h-screen grid-rows-layout">
|
||||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<SimpleNavbar />
|
||||
<Container>
|
||||
<div className="flex items-center justify-center w-full h-full isolate">
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { useLoaderData, useRevalidator } from "react-router-dom";
|
||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import { LinkButton } from "@components/Button";
|
||||
import KvmCard from "@components/KvmCard";
|
||||
import useInterval from "@/hooks/useInterval";
|
||||
import { checkAuth } from "@/main";
|
||||
import { User } from "@/hooks/stores";
|
||||
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";
|
||||
|
||||
interface LoaderData {
|
||||
|
@ -16,7 +16,7 @@ interface LoaderData {
|
|||
user: User;
|
||||
}
|
||||
|
||||
export const loader = async () => {
|
||||
const loader = async () => {
|
||||
const user = await checkAuth();
|
||||
|
||||
try {
|
||||
|
@ -40,7 +40,7 @@ export default function DevicesRoute() {
|
|||
useInterval(revalidate.revalidate, 4000);
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<div className="grid h-full select-none grid-rows-headerBody">
|
||||
<div className="grid h-full select-none grid-rows-(--grid-headerBody)">
|
||||
<DashboardNavbar
|
||||
isLoggedIn={!!user}
|
||||
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]}
|
||||
|
@ -101,3 +101,5 @@ export default function DevicesRoute() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DevicesRoute.loader = loader;
|
||||
|
|
|
@ -56,7 +56,7 @@ export default function LoginLocalRoute() {
|
|||
return (
|
||||
<>
|
||||
<GridBackground />
|
||||
<div className="grid min-h-screen grid-rows-layout">
|
||||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||
<SimpleNavbar />
|
||||
<Container>
|
||||
<div className="isolate flex h-full w-full items-center justify-center">
|
||||
|
|
|
@ -14,7 +14,6 @@ import api from "../api";
|
|||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
|
||||
const loader = async () => {
|
||||
const res = await api
|
||||
.GET(`${DEVICE_API}/device/status`)
|
||||
|
@ -59,18 +58,24 @@ export default function WelcomeLocalModeRoute() {
|
|||
<GridBackground />
|
||||
<div className="grid min-h-screen">
|
||||
<Container>
|
||||
<div className="flex items-center justify-center w-full h-full isolate">
|
||||
<div className="isolate flex h-full w-full items-center justify-center">
|
||||
<div className="max-w-xl space-y-8">
|
||||
<div className="flex items-center justify-center opacity-0 animate-fadeIn">
|
||||
<img src={LogoWhiteIcon} alt="" className="-ml-4 h-[32px] hidden dark:block" />
|
||||
<div className="animate-fadeIn flex items-center justify-center opacity-0">
|
||||
<img
|
||||
src={LogoWhiteIcon}
|
||||
alt=""
|
||||
className="-ml-4 hidden h-[32px] dark:block"
|
||||
/>
|
||||
<img src={LogoBlueIcon} alt="" className="-ml-4 h-[32px] dark:hidden" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-2 text-center opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn space-y-2 text-center opacity-0"
|
||||
style={{ animationDelay: "200ms" }}
|
||||
>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">Local Authentication Method</h1>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Local Authentication Method
|
||||
</h1>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400">
|
||||
Select how you{"'"}d like to secure your JetKVM device locally.
|
||||
</p>
|
||||
|
@ -78,7 +83,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
|
||||
<Form method="POST" className="space-y-8">
|
||||
<div
|
||||
className="grid grid-cols-1 gap-6 opacity-0 animate-fadeIn sm:grid-cols-2"
|
||||
className="animate-fadeIn grid grid-cols-1 gap-6 opacity-0 sm:grid-cols-2"
|
||||
style={{ animationDelay: "400ms" }}
|
||||
>
|
||||
{["password", "noPassword"].map(mode => (
|
||||
|
@ -90,14 +95,14 @@ export default function WelcomeLocalModeRoute() {
|
|||
})}
|
||||
>
|
||||
<div
|
||||
className="relative flex flex-col items-center p-6 cursor-pointer select-none"
|
||||
className="relative flex cursor-pointer flex-col items-center p-6 select-none"
|
||||
onClick={() => setSelectedMode(mode as "password" | "noPassword")}
|
||||
>
|
||||
<div className="space-y-0 text-center">
|
||||
<h3 className="text-base font-bold text-black dark:text-white">
|
||||
{mode === "password" ? "Password protected" : "No Password"}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-center text-gray-600 dark:text-gray-400">
|
||||
<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."}
|
||||
|
@ -111,7 +116,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
onChange={() => {
|
||||
setSelectedMode(mode as "password" | "noPassword");
|
||||
}}
|
||||
className="absolute w-4 h-4 text-blue-600 right-2 top-2"
|
||||
className="absolute top-2 right-2 h-4 w-4 text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
@ -120,7 +125,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
|
||||
{actionData?.error && (
|
||||
<p
|
||||
className="text-sm text-center text-red-600 opacity-0 dark:text-red-400 animate-fadeIn"
|
||||
className="animate-fadeIn text-center text-sm text-red-600 opacity-0 dark:text-red-400"
|
||||
style={{ animationDelay: "500ms" }}
|
||||
>
|
||||
{actionData.error}
|
||||
|
@ -128,7 +133,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
)}
|
||||
|
||||
<div
|
||||
className="max-w-sm mx-auto opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn mx-auto max-w-sm opacity-0"
|
||||
style={{ animationDelay: "500ms" }}
|
||||
>
|
||||
<Button
|
||||
|
@ -144,7 +149,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
</Form>
|
||||
|
||||
<p
|
||||
className="max-w-md mx-auto text-xs text-center opacity-0 animate-fadeIn text-slate-500 dark:text-slate-400"
|
||||
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.
|
||||
|
|
|
@ -69,28 +69,34 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
<GridBackground />
|
||||
<div className="grid min-h-screen">
|
||||
<Container>
|
||||
<div className="flex items-center justify-center w-full h-full isolate">
|
||||
<div className="isolate flex h-full w-full items-center justify-center">
|
||||
<div className="max-w-2xl space-y-8">
|
||||
<div className="flex items-center justify-center opacity-0 animate-fadeIn">
|
||||
<img src={LogoWhiteIcon} alt="" className="-ml-4 h-[32px] hidden dark:block" />
|
||||
<div className="animate-fadeIn flex items-center justify-center opacity-0">
|
||||
<img
|
||||
src={LogoWhiteIcon}
|
||||
alt=""
|
||||
className="-ml-4 hidden h-[32px] dark:block"
|
||||
/>
|
||||
<img src={LogoBlueIcon} alt="" className="-ml-4 h-[32px] dark:hidden" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-2 text-center opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn space-y-2 text-center opacity-0"
|
||||
style={{ animationDelay: "200ms" }}
|
||||
>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">Set a Password</h1>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Set a Password
|
||||
</h1>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400">
|
||||
Create a strong password to secure your JetKVM device locally.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Fieldset className="space-y-12">
|
||||
<Form method="POST" className="max-w-sm mx-auto space-y-4">
|
||||
<Form method="POST" className="mx-auto max-w-sm space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{ animationDelay: "400ms" }}
|
||||
>
|
||||
<InputFieldWithLabel
|
||||
|
@ -106,21 +112,21 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
onClick={() => setShowPassword(false)}
|
||||
className="pointer-events-auto"
|
||||
>
|
||||
<LuEye className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setShowPassword(true)}
|
||||
className="pointer-events-auto"
|
||||
>
|
||||
<LuEyeOff className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{ animationDelay: "400ms" }}
|
||||
>
|
||||
<InputFieldWithLabel
|
||||
|
@ -137,7 +143,7 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
{actionData?.error && <p className="text-sm text-red-600">{}</p>}
|
||||
|
||||
<div
|
||||
className="opacity-0 animate-fadeIn"
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{ animationDelay: "600ms" }}
|
||||
>
|
||||
<Button
|
||||
|
@ -153,7 +159,7 @@ export default function WelcomeLocalPasswordRoute() {
|
|||
</Fieldset>
|
||||
|
||||
<p
|
||||
className="max-w-md text-xs text-center opacity-0 animate-fadeIn text-slate-500 dark:text-slate-400"
|
||||
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
|
||||
|
|
|
@ -13,8 +13,6 @@ import { DEVICE_API } from "@/ui.config";
|
|||
|
||||
import api from "../api";
|
||||
|
||||
|
||||
|
||||
export interface DeviceStatus {
|
||||
isSetup: boolean;
|
||||
}
|
||||
|
@ -43,19 +41,24 @@ export default function WelcomeRoute() {
|
|||
<div className="grid min-h-screen">
|
||||
{imageLoaded && (
|
||||
<Container>
|
||||
<div className="flex items-center justify-center w-full h-full isolate">
|
||||
<div className="isolate flex h-full w-full items-center justify-center">
|
||||
<div className="max-w-3xl text-center">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center opacity-0 animate-fadeIn animation-delay-1000">
|
||||
<img src={LogoWhiteIcon} alt="JetKVM Logo" className="h-[32px] hidden dark:block" />
|
||||
<img src={LogoBlueIcon} alt="JetKVM Logo" className="h-[32px] dark:hidden" />
|
||||
<div className="animate-fadeIn animation-delay-1000 flex items-center justify-center opacity-0">
|
||||
<img
|
||||
src={LogoWhiteIcon}
|
||||
alt="JetKVM Logo"
|
||||
className="hidden h-[32px] dark:block"
|
||||
/>
|
||||
<img
|
||||
src={LogoBlueIcon}
|
||||
alt="JetKVM Logo"
|
||||
className="h-[32px] dark:hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-1 opacity-0 animate-fadeIn"
|
||||
style={{ animationDelay: "1500ms" }}
|
||||
>
|
||||
<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
|
||||
</h1>
|
||||
|
@ -69,22 +72,19 @@ export default function WelcomeRoute() {
|
|||
<img
|
||||
src={DeviceImage}
|
||||
alt="JetKVM Device"
|
||||
className="animation-delay-0 max-w-md scale-[0.98] animate-fadeInScaleFloat opacity-0 transition-all duration-1000 ease-out"
|
||||
className="animation-delay-300 animate-fadeInScaleFloat max-w-md scale-[0.98] opacity-0 transition-all duration-1000 ease-out"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mt-8 space-y-4">
|
||||
<p
|
||||
style={{ animationDelay: "2000ms" }}
|
||||
className="max-w-lg mx-auto text-lg opacity-0 animate-fadeIn text-slate-700 dark:text-slate-300"
|
||||
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.
|
||||
</p>
|
||||
<div
|
||||
style={{ animationDelay: "2300ms" }}
|
||||
className="opacity-0 animate-fadeIn"
|
||||
>
|
||||
<div className="animate-fadeIn animation-delay-2300 opacity-0">
|
||||
<LinkButton
|
||||
size="LG"
|
||||
theme="light"
|
||||
|
|
|
@ -5,98 +5,9 @@ import plugin from "tailwindcss/plugin";
|
|||
|
||||
/** @type {import("tailwindcss").Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{ts,tsx,svg}", "./index.html"],
|
||||
darkMode: "selector",
|
||||
theme: {
|
||||
extend: {
|
||||
gridTemplateRows: {
|
||||
layout: "auto 1fr auto",
|
||||
headerBody: "auto 1fr",
|
||||
bodyFooter: "1fr auto",
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
sidebar: "1fr minmax(360px, 25%)",
|
||||
},
|
||||
screens: {
|
||||
xs: "480px",
|
||||
"2xl": "1440px",
|
||||
"3xl": "1920px",
|
||||
"4xl": "2560px",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Circular", ...defaultTheme.fontFamily.sans],
|
||||
display: ["Circular", ...defaultTheme.fontFamily.sans],
|
||||
mono: ["Source Code Pro Variable", ...defaultTheme.fontFamily.mono],
|
||||
},
|
||||
maxWidth: {
|
||||
"8xl": "88rem",
|
||||
"9xl": "96rem",
|
||||
"10xl": "104rem",
|
||||
"11xl": "112rem",
|
||||
"12xl": "120rem",
|
||||
},
|
||||
animation: {
|
||||
enter: "enter .2s ease-out",
|
||||
leave: "leave .15s ease-in forwards",
|
||||
fadeInScale: "fadeInScale 1s ease-out forwards",
|
||||
fadeInScaleFloat:
|
||||
"fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite",
|
||||
fadeIn: "fadeIn 1s ease-out forwards",
|
||||
slideUpFade: "slideUpFade 1s ease-out forwards",
|
||||
},
|
||||
animationDelay: {
|
||||
1000: "1000ms",
|
||||
1500: "1500ms",
|
||||
},
|
||||
keyframes: {
|
||||
enter: {
|
||||
"0%": {
|
||||
opacity: "0",
|
||||
transform: "scale(.9)",
|
||||
},
|
||||
"100%": {
|
||||
opacity: "1",
|
||||
transform: "scale(1)",
|
||||
},
|
||||
},
|
||||
leave: {
|
||||
"0%": {
|
||||
opacity: "1",
|
||||
transform: "scale(1)",
|
||||
},
|
||||
"100%": {
|
||||
opacity: "0",
|
||||
transform: "scale(.9)",
|
||||
},
|
||||
},
|
||||
fadeInScale: {
|
||||
"0%": { opacity: "0", transform: "scale(0.98)" },
|
||||
"100%": { opacity: "1", transform: "scale(1)" },
|
||||
},
|
||||
fadeInScaleFloat: {
|
||||
"0%": { opacity: "0", transform: "scale(0.98) translateY(10px)" },
|
||||
"100%": { opacity: "1", transform: "scale(1) translateY(0)" },
|
||||
},
|
||||
float: {
|
||||
"0%, 100%": { transform: "translateY(0)" },
|
||||
"50%": { transform: "translateY(-10px)" },
|
||||
},
|
||||
fadeIn: {
|
||||
"0%": { opacity: "0", transform: "translateY(10px)" },
|
||||
"70%": { opacity: "0.8", transform: "translateY(1px)" },
|
||||
"100%": { opacity: "1", transform: "translateY(0)" },
|
||||
},
|
||||
slideUpFade: {
|
||||
"0%": { opacity: "0", transform: "translateY(20px)" },
|
||||
"100%": { opacity: "1", transform: "translateY(0)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("@headlessui/tailwindcss"),
|
||||
plugin(function ({ addVariant }) {
|
||||
addVariant("disabled-within", `&:has(input:is(:disabled),button:is(:disabled))`);
|
||||
}),
|
||||
|
@ -142,12 +53,5 @@ export default {
|
|||
},
|
||||
);
|
||||
},
|
||||
function ({ addUtilities, theme }) {
|
||||
const animationDelays = theme("animationDelay");
|
||||
const utilities = Object.entries(animationDelays).map(([key, value]) => ({
|
||||
[`.animation-delay-${key}`]: { animationDelay: value },
|
||||
}));
|
||||
addUtilities(utilities);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import basicSsl from "@vitejs/plugin-basic-ssl";
|
||||
|
||||
|
@ -16,7 +17,11 @@ export default defineConfig(({ mode, command }) => {
|
|||
const { JETKVM_PROXY_URL, USE_SSL } = process.env;
|
||||
const useSSL = USE_SSL === "true";
|
||||
|
||||
const plugins = [tsconfigPaths(), react()];
|
||||
const plugins = [
|
||||
tailwindcss(),
|
||||
tsconfigPaths(),
|
||||
react()
|
||||
];
|
||||
if (useSSL) {
|
||||
plugins.push(basicSsl());
|
||||
}
|
||||
|
|
4
usb.go
4
usb.go
|
@ -26,8 +26,8 @@ func initUsbGadget() {
|
|||
}()
|
||||
}
|
||||
|
||||
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
||||
return gadget.KeyboardReport(modifier, keys)
|
||||
func rpcKeyboardReport(modifier uint8, keys []uint8, hold bool) error {
|
||||
return gadget.KeyboardReport(modifier, keys, hold)
|
||||
}
|
||||
|
||||
func rpcAbsMouseReport(x, y int, buttons uint8) error {
|
||||
|
|
|
@ -26,6 +26,19 @@ func writeFile(path string, data string) error {
|
|||
return os.WriteFile(path, []byte(data), 0644)
|
||||
}
|
||||
|
||||
func getMassStorageImage() (string, error) {
|
||||
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get mass storage path: %w", err)
|
||||
}
|
||||
|
||||
imagePath, err := os.ReadFile(path.Join(massStorageFunctionPath, "file"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get mass storage image path: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(imagePath)), nil
|
||||
}
|
||||
|
||||
func setMassStorageImage(imagePath string) error {
|
||||
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
|
||||
if err != nil {
|
||||
|
@ -39,19 +52,21 @@ func setMassStorageImage(imagePath string) error {
|
|||
}
|
||||
|
||||
func setMassStorageMode(cdrom bool) error {
|
||||
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get mass storage path: %w", err)
|
||||
}
|
||||
|
||||
mode := "0"
|
||||
if cdrom {
|
||||
mode = "1"
|
||||
}
|
||||
if err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode); err != nil {
|
||||
|
||||
err, changed := gadget.OverrideGadgetConfig("mass_storage_lun0", "cdrom", mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set cdrom mode: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gadget.UpdateGadgetConfig()
|
||||
}
|
||||
|
||||
func onDiskMessage(msg webrtc.DataChannelMessage) {
|
||||
|
@ -79,9 +94,20 @@ var nbdDevice *NBDDevice
|
|||
|
||||
const imagesFolder = "/userdata/jetkvm/images"
|
||||
|
||||
func initImagesFolder() error {
|
||||
err := os.MkdirAll(imagesFolder, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create images folder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcMountBuiltInImage(filename string) error {
|
||||
logger.Info().Str("filename", filename).Msg("Mount Built-In Image")
|
||||
_ = os.MkdirAll(imagesFolder, 0755)
|
||||
if err := initImagesFolder(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(imagesFolder, filename)
|
||||
|
||||
// Check if the file exists in the imagesFolder
|
||||
|
@ -113,20 +139,17 @@ func rpcMountBuiltInImage(filename string) error {
|
|||
return mountImage(imagePath)
|
||||
}
|
||||
|
||||
func getMassStorageMode() (bool, error) {
|
||||
func getMassStorageCDROMEnabled() (bool, error) {
|
||||
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get mass storage path: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"))
|
||||
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "cdrom"))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read cdrom mode: %w", err)
|
||||
}
|
||||
|
||||
// Trim any whitespace characters. It has a newline at the end
|
||||
trimmedData := strings.TrimSpace(string(data))
|
||||
|
||||
return trimmedData == "1", nil
|
||||
}
|
||||
|
||||
|
@ -191,6 +214,61 @@ func rpcUnmountImage() error {
|
|||
|
||||
var httpRangeReader *httpreadat.RangeReader
|
||||
|
||||
func getInitialVirtualMediaState() (*VirtualMediaState, error) {
|
||||
cdromEnabled, err := getMassStorageCDROMEnabled()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get mass storage cdrom enabled: %w", err)
|
||||
}
|
||||
|
||||
diskPath, err := getMassStorageImage()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get mass storage image: %w", err)
|
||||
}
|
||||
|
||||
initialState := &VirtualMediaState{
|
||||
Source: Storage,
|
||||
Mode: Disk,
|
||||
}
|
||||
|
||||
if cdromEnabled {
|
||||
initialState.Mode = CDROM
|
||||
}
|
||||
|
||||
// TODO: check if it's WebRTC or HTTP
|
||||
switch diskPath {
|
||||
case "":
|
||||
return nil, nil
|
||||
case "/dev/nbd0":
|
||||
initialState.Source = HTTP
|
||||
initialState.URL = "/"
|
||||
initialState.Size = 1
|
||||
default:
|
||||
initialState.Filename = filepath.Base(diskPath)
|
||||
// get size from file
|
||||
logger.Info().Str("diskPath", diskPath).Msg("getting file size")
|
||||
info, err := os.Stat(diskPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file info: %w", err)
|
||||
}
|
||||
initialState.Size = info.Size()
|
||||
}
|
||||
|
||||
return initialState, nil
|
||||
}
|
||||
|
||||
func setInitialVirtualMediaState() error {
|
||||
virtualMediaStateMutex.Lock()
|
||||
defer virtualMediaStateMutex.Unlock()
|
||||
initialState, err := getInitialVirtualMediaState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get initial virtual media state: %w", err)
|
||||
}
|
||||
currentVirtualMediaState = initialState
|
||||
|
||||
logger.Info().Interface("initial_virtual_media_state", initialState).Msg("initial virtual media state set")
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcMountWithHTTP(url string, mode VirtualMediaMode) error {
|
||||
virtualMediaStateMutex.Lock()
|
||||
if currentVirtualMediaState != nil {
|
||||
|
@ -204,6 +282,11 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error {
|
|||
return fmt.Errorf("failed to use http url: %w", err)
|
||||
}
|
||||
logger.Info().Str("url", url).Int64("size", n).Msg("using remote url")
|
||||
|
||||
if err := setMassStorageMode(mode == CDROM); err != nil {
|
||||
return fmt.Errorf("failed to set mass storage mode: %w", err)
|
||||
}
|
||||
|
||||
currentVirtualMediaState = &VirtualMediaState{
|
||||
Source: HTTP,
|
||||
Mode: mode,
|
||||
|
@ -243,6 +326,11 @@ func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) erro
|
|||
Size: size,
|
||||
}
|
||||
virtualMediaStateMutex.Unlock()
|
||||
|
||||
if err := setMassStorageMode(mode == CDROM); err != nil {
|
||||
return fmt.Errorf("failed to set mass storage mode: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState")
|
||||
logger.Debug().Msg("Starting nbd device")
|
||||
nbdDevice = NewNBDDevice()
|
||||
|
@ -280,6 +368,10 @@ func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
|
|||
return fmt.Errorf("failed to get file info: %w", err)
|
||||
}
|
||||
|
||||
if err := setMassStorageMode(mode == CDROM); err != nil {
|
||||
return fmt.Errorf("failed to set mass storage mode: %w", err)
|
||||
}
|
||||
|
||||
err = setMassStorageImage(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set mass storage image: %w", err)
|
||||
|
|
Loading…
Reference in New Issue