mirror of https://github.com/jetkvm/kvm.git
Compare commits
77 Commits
9bde820321
...
fe525676be
Author | SHA1 | Date |
---|---|---|
|
fe525676be | |
|
5aa46c06a0 | |
|
ab3dda6dee | |
|
4a23f22a55 | |
|
11a095c0f6 | |
|
584768bacf | |
|
488276f3a8 | |
|
7267347261 | |
|
393bc122d4 | |
|
6d13e1be12 | |
|
bde0a086ab | |
|
9c9335da31 | |
|
090e0b4b47 | |
|
48a7a638a3 | |
|
e4f6a713a5 | |
|
9fcf74b398 | |
|
353099001f | |
|
73f5659618 | |
|
960f555790 | |
|
fe127ed41c | |
|
3e7d8fb0f5 | |
|
0d7f47c109 | |
|
254c001572 | |
|
6f037a832d | |
|
ccba27cedd | |
|
cf9c6e5cc8 | |
|
ffeaf8cced | |
|
a1ed28c676 | |
|
1674a6666c | |
|
772527849f | |
|
19871517ec | |
|
b822b73a03 | |
|
58ade3b551 | |
|
3cc119c646 | |
|
c494cf26ef | |
|
4bfbc66ea7 | |
|
0636cc9aff | |
|
4f6026e182 | |
|
89f3bc8c40 | |
|
91171d9bf7 | |
|
0d955a8d95 | |
|
a40d26ab9b | |
|
9bd587b52e | |
|
7ef9a7ba93 | |
|
bfbc1a5a57 | |
|
abb4350316 | |
|
52825da68d | |
|
9d2abd9fb0 | |
|
52dd675e52 | |
|
e95e30e48c | |
|
eaa58492ab | |
|
f4bb47c544 | |
|
a7693df92c | |
|
8d77d75294 | |
|
718b343713 | |
|
1f7c5c94d8 | |
|
55d7f22c47 | |
|
a28676cd94 | |
|
2ec061b3a8 | |
|
7e64a529f8 | |
|
1b5062c504 | |
|
c1d771cced | |
|
019934d33e | |
|
0c5c69f2d3 | |
|
0cee284561 | |
|
2272247668 | |
|
21e30c60ea | |
|
25e30f6420 | |
|
b91a995918 | |
|
590c606bb1 | |
|
a60e1a5e98 | |
|
4e90883bf8 | |
|
8eaa86ae45 | |
|
354941b54d | |
|
4b91c758fa | |
|
222a8470a5 | |
|
860327bfcd |
|
@ -9,6 +9,19 @@
|
|||
},
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
||||
]
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"golang.go",
|
||||
"ms-vscode.makefile-tools",
|
||||
"esbenp.prettier-vscode",
|
||||
"github.vscode-github-actions"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: npm
|
||||
directory: /ui
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: monthly
|
|
@ -10,22 +10,22 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
name: Build
|
||||
if: "github.event.review.state == 'approved' || github.event.event_type != 'pull_request_review'"
|
||||
if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v21.1.0
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5.5.0
|
||||
with:
|
||||
go-version: "1.24.0"
|
||||
go-version: "1.24.4"
|
||||
- name: Build frontend
|
||||
run: |
|
||||
make frontend
|
||||
|
|
|
@ -24,9 +24,9 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
go-version: 1.24.4
|
||||
- name: Create empty resource directory
|
||||
run: |
|
||||
mkdir -p static && touch static/.gitkeep
|
||||
|
|
|
@ -104,9 +104,9 @@ jobs:
|
|||
EOF
|
||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5.5.0
|
||||
with:
|
||||
go-version: "1.24.0"
|
||||
go-version: "1.24.4"
|
||||
- name: Golang Test Report
|
||||
uses: becheran/go-testreport@v0.3.2
|
||||
with:
|
||||
|
|
|
@ -14,16 +14,16 @@ permissions:
|
|||
jobs:
|
||||
ui-lint:
|
||||
name: UI Lint
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v21.1.0
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "ui/package-lock.json"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd ui
|
||||
|
|
|
@ -4,6 +4,7 @@ linters:
|
|||
- forbidigo
|
||||
- misspell
|
||||
- whitespace
|
||||
- gochecknoinits
|
||||
settings:
|
||||
forbidigo:
|
||||
forbid:
|
||||
|
@ -25,6 +26,9 @@ linters:
|
|||
- linters:
|
||||
- forbidigo
|
||||
path: cmd/main.go
|
||||
- linters:
|
||||
- gochecknoinits
|
||||
path: internal/logging/sse.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tailwindCSS.classFunctions": ["cva", "cx"]
|
||||
}
|
17
Makefile
17
Makefile
|
@ -2,12 +2,14 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
|||
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
||||
BUILDTS ?= $(shell date -u +%s)
|
||||
REVISION ?= $(shell git rev-parse HEAD)
|
||||
VERSION_DEV := 0.4.0-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION := 0.3.9
|
||||
VERSION_DEV ?= 0.4.7-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION ?= 0.4.6
|
||||
|
||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||
KVM_PKG_NAME := github.com/jetkvm/kvm
|
||||
|
||||
GO_BUILD_ARGS := -tags netgo
|
||||
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
|
||||
GO_LDFLAGS := \
|
||||
-s -w \
|
||||
-X $(PROMETHEUS_TAG).Branch=$(BRANCH) \
|
||||
|
@ -25,7 +27,10 @@ hash_resource:
|
|||
|
||||
build_dev: hash_resource
|
||||
@echo "Building..."
|
||||
$(GO_CMD) build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o $(BIN_DIR)/jetkvm_app cmd/main.go
|
||||
$(GO_CMD) build \
|
||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
||||
$(GO_RELEASE_BUILD_ARGS) \
|
||||
-o $(BIN_DIR)/jetkvm_app cmd/main.go
|
||||
|
||||
build_test2json:
|
||||
$(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json
|
||||
|
@ -47,6 +52,7 @@ build_dev_test: build_test2json build_gotestsum
|
|||
test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \
|
||||
$(GO_CMD) test -v \
|
||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
||||
$(GO_BUILD_ARGS) \
|
||||
-c -o $(BIN_DIR)/tests/$$test_filename $$test; \
|
||||
echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \
|
||||
done; \
|
||||
|
@ -66,7 +72,10 @@ dev_release: frontend build_dev
|
|||
|
||||
build_release: frontend hash_resource
|
||||
@echo "Building release..."
|
||||
$(GO_CMD) build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go
|
||||
$(GO_CMD) build \
|
||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \
|
||||
$(GO_RELEASE_BUILD_ARGS) \
|
||||
-o bin/jetkvm_app cmd/main.go
|
||||
|
||||
release:
|
||||
@if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \
|
||||
|
|
22
cloud.go
22
cloud.go
|
@ -51,34 +51,34 @@ var (
|
|||
)
|
||||
metricCloudConnectionEstablishedTimestamp = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_cloud_connection_established_timestamp",
|
||||
Name: "jetkvm_cloud_connection_established_timestamp_seconds",
|
||||
Help: "The timestamp when the cloud connection was established",
|
||||
},
|
||||
)
|
||||
metricConnectionLastPingTimestamp = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_connection_last_ping_timestamp",
|
||||
Name: "jetkvm_connection_last_ping_timestamp_seconds",
|
||||
Help: "The timestamp when the last ping response was received",
|
||||
},
|
||||
[]string{"type", "source"},
|
||||
)
|
||||
metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_connection_last_ping_received_timestamp",
|
||||
Name: "jetkvm_connection_last_ping_received_timestamp_seconds",
|
||||
Help: "The timestamp when the last ping request was received",
|
||||
},
|
||||
[]string{"type", "source"},
|
||||
)
|
||||
metricConnectionLastPingDuration = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_connection_last_ping_duration",
|
||||
Name: "jetkvm_connection_last_ping_duration_seconds",
|
||||
Help: "The duration of the last ping response",
|
||||
},
|
||||
[]string{"type", "source"},
|
||||
)
|
||||
metricConnectionPingDuration = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "jetkvm_connection_ping_duration",
|
||||
Name: "jetkvm_connection_ping_duration_seconds",
|
||||
Help: "The duration of the ping response",
|
||||
Buckets: []float64{
|
||||
0.1, 0.5, 1, 10,
|
||||
|
@ -88,28 +88,28 @@ var (
|
|||
)
|
||||
metricConnectionTotalPingSentCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_connection_total_ping_sent",
|
||||
Name: "jetkvm_connection_ping_sent_total",
|
||||
Help: "The total number of pings sent to the connection",
|
||||
},
|
||||
[]string{"type", "source"},
|
||||
)
|
||||
metricConnectionTotalPingReceivedCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_connection_total_ping_received",
|
||||
Name: "jetkvm_connection_ping_received_total",
|
||||
Help: "The total number of pings received from the connection",
|
||||
},
|
||||
[]string{"type", "source"},
|
||||
)
|
||||
metricConnectionSessionRequestCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_connection_session_total_requests",
|
||||
Name: "jetkvm_connection_session_requests_total",
|
||||
Help: "The total number of session requests received",
|
||||
},
|
||||
[]string{"type", "source"},
|
||||
)
|
||||
metricConnectionSessionRequestDuration = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "jetkvm_connection_session_request_duration",
|
||||
Name: "jetkvm_connection_session_request_duration_seconds",
|
||||
Help: "The duration of session requests",
|
||||
Buckets: []float64{
|
||||
0.1, 0.5, 1, 10,
|
||||
|
@ -119,7 +119,7 @@ var (
|
|||
)
|
||||
metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_connection_last_session_request_timestamp",
|
||||
Name: "jetkvm_connection_last_session_request_timestamp_seconds",
|
||||
Help: "The timestamp of the last session request",
|
||||
},
|
||||
[]string{"type", "source"},
|
||||
|
@ -133,7 +133,7 @@ var (
|
|||
)
|
||||
metricCloudConnectionFailureCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_cloud_connection_failure_count",
|
||||
Name: "jetkvm_cloud_connection_failure_total",
|
||||
Help: "The number of times the cloud connection has failed",
|
||||
},
|
||||
)
|
||||
|
|
|
@ -85,8 +85,10 @@ type Config struct {
|
|||
HashedPassword string `json:"hashed_password"`
|
||||
LocalAuthToken string `json:"local_auth_token"`
|
||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||
LocalLoopbackOnly bool `json:"local_loopback_only"`
|
||||
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"`
|
||||
|
@ -109,6 +111,7 @@ var defaultConfig = &Config{
|
|||
ActiveExtension: "",
|
||||
KeyboardMacros: []KeyboardMacro{},
|
||||
DisplayRotation: "270",
|
||||
KeyboardLayout: "en_US",
|
||||
DisplayMaxBrightness: 64,
|
||||
DisplayDimAfterSec: 120, // 2 minutes
|
||||
DisplayOffAfterSec: 1800, // 30 minutes
|
||||
|
|
44
display.go
44
display.go
|
@ -9,9 +9,14 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
var currentScreen = "ui_Boot_Screen"
|
||||
var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
|
||||
|
||||
var (
|
||||
currentScreen = "ui_Boot_Screen"
|
||||
displayedTexts = make(map[string]string)
|
||||
screenStateLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
var (
|
||||
dimTicker *time.Ticker
|
||||
offTicker *time.Ticker
|
||||
|
@ -22,6 +27,8 @@ const (
|
|||
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
|
||||
)
|
||||
|
||||
// do not call this function directly, use switchToScreenIfDifferent instead
|
||||
// this function is not thread safe
|
||||
func switchToScreen(screen string) {
|
||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
||||
if err != nil {
|
||||
|
@ -31,8 +38,6 @@ func switchToScreen(screen string) {
|
|||
currentScreen = screen
|
||||
}
|
||||
|
||||
var displayedTexts = make(map[string]string)
|
||||
|
||||
func lvObjSetState(objName string, state string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state})
|
||||
}
|
||||
|
@ -78,6 +83,9 @@ func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
|
|||
}
|
||||
|
||||
func updateLabelIfChanged(objName string, newText string) {
|
||||
screenStateLock.Lock()
|
||||
defer screenStateLock.Unlock()
|
||||
|
||||
if newText != "" && newText != displayedTexts[objName] {
|
||||
_, _ = lvLabelSetText(objName, newText)
|
||||
displayedTexts[objName] = newText
|
||||
|
@ -85,12 +93,23 @@ func updateLabelIfChanged(objName string, newText string) {
|
|||
}
|
||||
|
||||
func switchToScreenIfDifferent(screenName string) {
|
||||
screenStateLock.Lock()
|
||||
defer screenStateLock.Unlock()
|
||||
|
||||
if currentScreen != screenName {
|
||||
displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen")
|
||||
switchToScreen(screenName)
|
||||
}
|
||||
}
|
||||
|
||||
func clearDisplayState() {
|
||||
screenStateLock.Lock()
|
||||
defer screenStateLock.Unlock()
|
||||
|
||||
displayedTexts = make(map[string]string)
|
||||
currentScreen = "ui_Boot_Screen"
|
||||
}
|
||||
|
||||
var (
|
||||
cloudBlinkLock sync.Mutex = sync.Mutex{}
|
||||
cloudBlinkStopped bool
|
||||
|
@ -339,10 +358,18 @@ func startBacklightTickers() {
|
|||
return
|
||||
}
|
||||
|
||||
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
||||
// Stop existing tickers to prevent multiple active instances on repeated calls
|
||||
if dimTicker != nil {
|
||||
dimTicker.Stop()
|
||||
}
|
||||
|
||||
if offTicker != nil {
|
||||
offTicker.Stop()
|
||||
}
|
||||
|
||||
if config.DisplayDimAfterSec != 0 {
|
||||
displayLogger.Info().Msg("dim_ticker has started")
|
||||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||
defer dimTicker.Stop()
|
||||
|
||||
go func() {
|
||||
for { //nolint:staticcheck
|
||||
|
@ -354,10 +381,9 @@ func startBacklightTickers() {
|
|||
}()
|
||||
}
|
||||
|
||||
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
||||
if config.DisplayOffAfterSec != 0 {
|
||||
displayLogger.Info().Msg("off_ticker has started")
|
||||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||
defer offTicker.Stop()
|
||||
|
||||
go func() {
|
||||
for { //nolint:staticcheck
|
||||
|
@ -370,9 +396,7 @@ func startBacklightTickers() {
|
|||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
ensureConfigLoaded()
|
||||
|
||||
func initDisplay() {
|
||||
go func() {
|
||||
waitCtrlClientConnected()
|
||||
displayLogger.Info().Msg("setting initial display contents")
|
||||
|
|
88
go.mod
88
go.mod
|
@ -1,37 +1,35 @@
|
|||
module github.com/jetkvm/kvm
|
||||
|
||||
go 1.23.4
|
||||
|
||||
toolchain go1.24.3
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.3.0
|
||||
github.com/beevik/ntp v1.3.1
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/creack/pty v1.1.23
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.5
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/webrtc/v4 v4.0.0
|
||||
github.com/pion/webrtc/v4 v4.1.3
|
||||
github.com/pojntfx/go-nbd v0.3.2
|
||||
github.com/prometheus/client_golang v1.21.0
|
||||
github.com/prometheus/common v0.62.0
|
||||
github.com/prometheus/procfs v0.15.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/common v0.65.0
|
||||
github.com/prometheus/procfs v0.16.1
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
go.bug.st/serial v1.6.2
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.bug.st/serial v1.6.4
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
|
@ -39,22 +37,20 @@ replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20
|
|||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/creack/goselect v0.1.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
|
@ -62,31 +58,31 @@ require (
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pilebones/go-udev v0.9.0 // indirect
|
||||
github.com/pion/datachannel v1.5.9 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.3 // indirect
|
||||
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||
github.com/pion/interceptor v0.1.37 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pilebones/go-udev v0.9.1 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.9 // indirect
|
||||
github.com/pion/sctp v1.8.33 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.20 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.14 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.6 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
178
go.sum
178
go.sum
|
@ -1,11 +1,11 @@
|
|||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=
|
||||
github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
|
@ -18,28 +18,28 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
|
|||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
|
||||
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM=
|
||||
github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc=
|
||||
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
@ -58,14 +58,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoNJH4uUmsQ86+0/QqpwEns0NyNLwKv0=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
|
@ -77,7 +77,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
|
@ -89,8 +88,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -98,57 +97,57 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
||||
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
|
||||
github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
|
||||
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
|
||||
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
|
||||
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
||||
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
|
||||
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=
|
||||
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
|
||||
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
|
||||
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
|
||||
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
|
@ -157,38 +156,33 @@ github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzr
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
||||
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -196,8 +190,8 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -43,9 +43,11 @@ type testNetworkConfig struct {
|
|||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||
}
|
||||
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
|
|
|
@ -45,9 +45,11 @@ type NetworkConfig struct {
|
|||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||
}
|
||||
|
||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||
|
|
|
@ -21,6 +21,7 @@ type NetworkInterfaceState struct {
|
|||
ipv6Addr *net.IP
|
||||
ipv6Addresses []IPv6Address
|
||||
ipv6LinkLocal *net.IP
|
||||
ntpAddresses []*net.IP
|
||||
macAddr *net.HardwareAddr
|
||||
|
||||
l *zerolog.Logger
|
||||
|
@ -76,6 +77,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
onInitialCheck: opts.OnInitialCheck,
|
||||
cbConfigChange: opts.OnConfigChange,
|
||||
config: opts.NetworkConfig,
|
||||
ntpAddresses: make([]*net.IP, 0),
|
||||
}
|
||||
|
||||
// create the dhcp client
|
||||
|
@ -89,7 +91,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.updateNtpServersFromLease(lease)
|
||||
_ = s.setHostnameIfNotSame()
|
||||
|
||||
opts.OnDhcpLeaseChange(lease)
|
||||
|
@ -135,6 +137,27 @@ func (s *NetworkInterfaceState) IPv6String() string {
|
|||
return s.ipv6Addr.String()
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
|
||||
return s.ntpAddresses
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) NtpAddressesString() []string {
|
||||
ntpServers := []string{}
|
||||
|
||||
if s != nil {
|
||||
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
|
||||
|
||||
if len(s.ntpAddresses) > 0 {
|
||||
for _, server := range s.ntpAddresses {
|
||||
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
|
||||
ntpServers = append(ntpServers, server.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ntpServers
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
|
||||
return s.macAddr
|
||||
}
|
||||
|
@ -318,6 +341,25 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
return dhcpTargetState, nil
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
|
||||
if lease != nil && len(lease.NTPServers) > 0 {
|
||||
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
|
||||
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
|
||||
|
||||
for _, ntpServer := range lease.NTPServers {
|
||||
if ntpServer != nil {
|
||||
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
|
||||
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.l.Info().Msg("no NTP servers found in lease")
|
||||
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
||||
dhcpTargetState, err := s.update()
|
||||
if err != nil {
|
||||
|
|
|
@ -19,9 +19,9 @@ var defaultHTTPUrls = []string{
|
|||
// "http://www.msftconnecttest.com/connecttest.txt",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryAllHttpTime() (now *time.Time) {
|
||||
chunkSize := 4
|
||||
httpUrls := t.httpUrls
|
||||
func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs")
|
||||
|
||||
// shuffle the http urls to avoid always querying the same servers
|
||||
rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] })
|
||||
|
@ -95,16 +95,27 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
|
|||
} else if errors.Is(err, context.Canceled) {
|
||||
metricHttpCancelCount.WithLabelValues(url).Inc()
|
||||
metricHttpTotalCancelCount.Inc()
|
||||
results <- nil
|
||||
} else {
|
||||
scopedLogger.Warn().
|
||||
Str("error", err.Error()).
|
||||
Int("status", status).
|
||||
Msg("failed to query HTTP server")
|
||||
results <- nil
|
||||
}
|
||||
}(url)
|
||||
}
|
||||
|
||||
return <-results
|
||||
for range urls {
|
||||
result := <-results
|
||||
if result == nil {
|
||||
continue
|
||||
}
|
||||
now = result
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func queryHttpTime(
|
||||
|
|
|
@ -14,44 +14,44 @@ var (
|
|||
)
|
||||
metricTimeSyncCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_count",
|
||||
Name: "jetkvm_timesync_total",
|
||||
Help: "The number of times the timesync has been run",
|
||||
},
|
||||
)
|
||||
metricTimeSyncSuccessCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_success_count",
|
||||
Name: "jetkvm_timesync_success_total",
|
||||
Help: "The number of times the timesync has been successful",
|
||||
},
|
||||
)
|
||||
metricRTCUpdateCount = promauto.NewCounter( //nolint:unused
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_rtc_update_count",
|
||||
Name: "jetkvm_timesync_rtc_update_total",
|
||||
Help: "The number of times the RTC has been updated",
|
||||
},
|
||||
)
|
||||
metricNtpTotalSuccessCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_ntp_total_success_count",
|
||||
Name: "jetkvm_timesync_ntp_total_success_total",
|
||||
Help: "The total number of successful NTP requests",
|
||||
},
|
||||
)
|
||||
metricNtpTotalRequestCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_ntp_total_request_count",
|
||||
Name: "jetkvm_timesync_ntp_total_request_total",
|
||||
Help: "The total number of NTP requests sent",
|
||||
},
|
||||
)
|
||||
metricNtpSuccessCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_ntp_success_count",
|
||||
Name: "jetkvm_timesync_ntp_success_total",
|
||||
Help: "The number of successful NTP requests",
|
||||
},
|
||||
[]string{"url"},
|
||||
)
|
||||
metricNtpRequestCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_ntp_request_count",
|
||||
Name: "jetkvm_timesync_ntp_request_total",
|
||||
Help: "The number of NTP requests sent to the server",
|
||||
},
|
||||
[]string{"url"},
|
||||
|
@ -73,6 +73,7 @@ var (
|
|||
},
|
||||
[]string{"url"},
|
||||
)
|
||||
|
||||
metricNtpServerInfo = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_timesync_ntp_server_info",
|
||||
|
@ -83,39 +84,39 @@ var (
|
|||
|
||||
metricHttpTotalSuccessCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_http_total_success_count",
|
||||
Name: "jetkvm_timesync_http_total_success_total",
|
||||
Help: "The total number of successful HTTP requests",
|
||||
},
|
||||
)
|
||||
metricHttpTotalRequestCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_http_total_request_count",
|
||||
Name: "jetkvm_timesync_http_total_request_total",
|
||||
Help: "The total number of HTTP requests sent",
|
||||
},
|
||||
)
|
||||
metricHttpTotalCancelCount = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_http_total_cancel_count",
|
||||
Name: "jetkvm_timesync_http_total_cancel_total",
|
||||
Help: "The total number of HTTP requests cancelled",
|
||||
},
|
||||
)
|
||||
metricHttpSuccessCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_http_success_count",
|
||||
Name: "jetkvm_timesync_http_success_total",
|
||||
Help: "The number of successful HTTP requests",
|
||||
},
|
||||
[]string{"url"},
|
||||
)
|
||||
metricHttpRequestCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_http_request_count",
|
||||
Name: "jetkvm_timesync_http_request_total",
|
||||
Help: "The number of HTTP requests sent to the server",
|
||||
},
|
||||
[]string{"url"},
|
||||
)
|
||||
metricHttpCancelCount = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_timesync_http_cancel_count",
|
||||
Name: "jetkvm_timesync_http_cancel_total",
|
||||
Help: "The number of HTTP requests cancelled",
|
||||
},
|
||||
[]string{"url"},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package timesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -21,9 +22,9 @@ var defaultNTPServers = []string{
|
|||
"3.pool.ntp.org",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := 4
|
||||
ntpServers := t.ntpServers
|
||||
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")
|
||||
|
||||
// shuffle the ntp servers to avoid always querying the same servers
|
||||
rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] })
|
||||
|
@ -46,6 +47,10 @@ type ntpResult struct {
|
|||
|
||||
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
|
||||
results := make(chan *ntpResult, len(servers))
|
||||
|
||||
_, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
for _, server := range servers {
|
||||
go func(server string) {
|
||||
scopedLogger := t.l.With().
|
||||
|
@ -66,15 +71,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
return
|
||||
}
|
||||
|
||||
if response.IsKissOfDeath() {
|
||||
scopedLogger.Warn().
|
||||
Str("kiss_code", response.KissCode).
|
||||
Msg("ignoring NTP server kiss of death")
|
||||
results <- nil
|
||||
return
|
||||
}
|
||||
|
||||
rtt := float64(response.RTT.Milliseconds())
|
||||
|
||||
// set the last RTT
|
||||
metricNtpServerLastRTT.WithLabelValues(
|
||||
server,
|
||||
).Set(float64(response.RTT.Milliseconds()))
|
||||
).Set(rtt)
|
||||
|
||||
// set the RTT histogram
|
||||
metricNtpServerRttHistogram.WithLabelValues(
|
||||
server,
|
||||
).Observe(float64(response.RTT.Milliseconds()))
|
||||
).Observe(rtt)
|
||||
|
||||
// set the server info
|
||||
metricNtpServerInfo.WithLabelValues(
|
||||
|
@ -91,10 +106,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
scopedLogger.Info().
|
||||
Str("time", now.Format(time.RFC3339)).
|
||||
Str("reference", response.ReferenceString()).
|
||||
Str("rtt", response.RTT.String()).
|
||||
Float64("rtt", rtt).
|
||||
Str("clockOffset", response.ClockOffset.String()).
|
||||
Uint8("stratum", response.Stratum).
|
||||
Msg("NTP server returned time")
|
||||
|
||||
cancel()
|
||||
|
||||
results <- &ntpResult{
|
||||
now: now,
|
||||
offset: &response.ClockOffset,
|
||||
|
|
|
@ -28,9 +28,8 @@ type TimeSync struct {
|
|||
syncLock *sync.Mutex
|
||||
l *zerolog.Logger
|
||||
|
||||
ntpServers []string
|
||||
httpUrls []string
|
||||
networkConfig *network.NetworkConfig
|
||||
networkConfig *network.NetworkConfig
|
||||
dhcpNtpAddresses []string
|
||||
|
||||
rtcDevicePath string
|
||||
rtcDevice *os.File //nolint:unused
|
||||
|
@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
}
|
||||
|
||||
t := &TimeSync{
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
ntpServers: defaultNTPServers,
|
||||
httpUrls: defaultHTTPUrls,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
dhcpNtpAddresses: []string{},
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
}
|
||||
|
||||
if t.rtcDevicePath != "" {
|
||||
|
@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
return t
|
||||
}
|
||||
|
||||
func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) {
|
||||
t.dhcpNtpAddresses = addresses
|
||||
}
|
||||
|
||||
func (t *TimeSync) getSyncMode() SyncMode {
|
||||
syncMode := SyncMode{
|
||||
Ntp: true,
|
||||
Http: true,
|
||||
Ordering: []string{"ntp_dhcp", "ntp", "http"},
|
||||
NtpUseFallback: true,
|
||||
HttpUseFallback: true,
|
||||
}
|
||||
var syncModeString string
|
||||
|
||||
if t.networkConfig != nil {
|
||||
syncModeString = t.networkConfig.TimeSyncMode.String
|
||||
switch t.networkConfig.TimeSyncMode.String {
|
||||
case "ntp_only":
|
||||
syncMode.Http = false
|
||||
case "http_only":
|
||||
syncMode.Ntp = false
|
||||
}
|
||||
|
||||
if t.networkConfig.TimeSyncDisableFallback.Bool {
|
||||
syncMode.NtpUseFallback = false
|
||||
syncMode.HttpUseFallback = false
|
||||
}
|
||||
|
||||
var syncOrdering = t.networkConfig.TimeSyncOrdering
|
||||
if len(syncOrdering) > 0 {
|
||||
syncMode.Ordering = syncOrdering
|
||||
}
|
||||
}
|
||||
|
||||
switch syncModeString {
|
||||
case "ntp_only":
|
||||
syncMode.Ntp = true
|
||||
case "http_only":
|
||||
syncMode.Http = true
|
||||
default:
|
||||
syncMode.Ntp = true
|
||||
syncMode.Http = true
|
||||
}
|
||||
t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode")
|
||||
|
||||
return syncMode
|
||||
}
|
||||
|
||||
func (t *TimeSync) doTimeSync() {
|
||||
metricTimeSyncStatus.Set(0)
|
||||
for {
|
||||
|
@ -154,16 +160,61 @@ func (t *TimeSync) Sync() error {
|
|||
offset *time.Duration
|
||||
)
|
||||
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
metricTimeSyncCount.Inc()
|
||||
|
||||
if syncMode.Ntp {
|
||||
now, offset = t.queryNetworkTime()
|
||||
}
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
if syncMode.Http && now == nil {
|
||||
now = t.queryAllHttpTime()
|
||||
Orders:
|
||||
for _, mode := range syncMode.Ordering {
|
||||
switch mode {
|
||||
case "ntp_user_provided":
|
||||
if syncMode.Ntp {
|
||||
t.l.Info().Msg("using NTP custom servers")
|
||||
now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp_dhcp":
|
||||
if syncMode.Ntp {
|
||||
t.l.Info().Msg("using NTP servers from DHCP")
|
||||
now, offset = t.queryNetworkTime(t.dhcpNtpAddresses)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp":
|
||||
if syncMode.Ntp && syncMode.NtpUseFallback {
|
||||
t.l.Info().Msg("using NTP fallback")
|
||||
now, offset = t.queryNetworkTime(defaultNTPServers)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http_user_provided":
|
||||
if syncMode.Http {
|
||||
t.l.Info().Msg("using HTTP custom URLs")
|
||||
now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
if syncMode.Http && syncMode.HttpUseFallback {
|
||||
t.l.Info().Msg("using HTTP fallback")
|
||||
now = t.queryAllHttpTime(defaultHTTPUrls)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if now == nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
|
@ -149,6 +150,12 @@ func (c *DHCPClient) loadLeaseFile() error {
|
|||
}
|
||||
|
||||
isFirstLoad := c.lease == nil
|
||||
|
||||
// Skip processing if lease hasn't changed to avoid unnecessary wake-ups.
|
||||
if reflect.DeepEqual(c.lease, lease) {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.lease = lease
|
||||
|
||||
if lease.IPAddress == nil {
|
||||
|
|
|
@ -30,8 +30,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
|
|||
attrs: gadgetAttributes{
|
||||
"bcdUSB": "0x0200", // USB 2.0
|
||||
"idVendor": "0x1d6b", // The Linux Foundation
|
||||
"idProduct": "0104", // Multifunction Composite Gadget
|
||||
"bcdDevice": "0100",
|
||||
"idProduct": "0x0104", // Multifunction Composite Gadget
|
||||
"bcdDevice": "0x0100", // USB2
|
||||
},
|
||||
configAttrs: gadgetAttributes{
|
||||
"MaxPower": "250", // in unit of 2mA
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
var keyboardConfig = gadgetConfigItem{
|
||||
|
@ -36,6 +39,7 @@ var keyboardReportDesc = []byte{
|
|||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
||||
0x95, 0x05, /* REPORT_COUNT (5) */
|
||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||
|
||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
||||
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
||||
|
@ -54,23 +58,155 @@ var keyboardReportDesc = []byte{
|
|||
0xc0, /* END_COLLECTION */
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||
if u.keyboardHidFile == nil {
|
||||
var err error
|
||||
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||
const (
|
||||
hidReadBufferSize = 8
|
||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
||||
KeyboardLedMaskNumLock = 1 << 0
|
||||
KeyboardLedMaskCapsLock = 1 << 1
|
||||
KeyboardLedMaskScrollLock = 1 << 2
|
||||
KeyboardLedMaskCompose = 1 << 3
|
||||
KeyboardLedMaskKana = 1 << 4
|
||||
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
|
||||
)
|
||||
|
||||
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
||||
// COMPOSE, and KANA events is maintained by the host and NOT the keyboard. If
|
||||
// using the keyboard descriptor in Appendix B, LED states are set by sending a
|
||||
// 5-bit absolute report to the keyboard via a Set_Report(Output) request.
|
||||
type KeyboardState struct {
|
||||
NumLock bool `json:"num_lock"`
|
||||
CapsLock bool `json:"caps_lock"`
|
||||
ScrollLock bool `json:"scroll_lock"`
|
||||
Compose bool `json:"compose"`
|
||||
Kana bool `json:"kana"`
|
||||
}
|
||||
|
||||
func getKeyboardState(b byte) KeyboardState {
|
||||
// should we check if it's the correct usage page?
|
||||
return KeyboardState{
|
||||
NumLock: b&KeyboardLedMaskNumLock != 0,
|
||||
CapsLock: b&KeyboardLedMaskCapsLock != 0,
|
||||
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
||||
Compose: b&KeyboardLedMaskCompose != 0,
|
||||
Kana: b&KeyboardLedMaskKana != 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) updateKeyboardState(b byte) {
|
||||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
|
||||
if b&^ValidKeyboardLedMasks != 0 {
|
||||
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
newState := getKeyboardState(b)
|
||||
if reflect.DeepEqual(u.keyboardState, newState) {
|
||||
return
|
||||
}
|
||||
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated")
|
||||
u.keyboardState = newState
|
||||
|
||||
if u.onKeyboardStateChange != nil {
|
||||
(*u.onKeyboardStateChange)(newState)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) {
|
||||
u.onKeyboardStateChange = &f
|
||||
}
|
||||
|
||||
func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
||||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
|
||||
return u.keyboardState
|
||||
}
|
||||
|
||||
func (u *UsbGadget) listenKeyboardEvents() {
|
||||
var path string
|
||||
if u.keyboardHidFile != nil {
|
||||
path = u.keyboardHidFile.Name()
|
||||
}
|
||||
l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger()
|
||||
l.Trace().Msg("starting")
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, hidReadBufferSize)
|
||||
for {
|
||||
select {
|
||||
case <-u.keyboardStateCtx.Done():
|
||||
l.Info().Msg("context done")
|
||||
return
|
||||
default:
|
||||
l.Trace().Msg("reading from keyboard")
|
||||
if u.keyboardHidFile == nil {
|
||||
u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
|
||||
// show the error every 100 times to avoid spamming the logs
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
// reset the counter
|
||||
u.resetLogSuppressionCounter("keyboardHidFileNil")
|
||||
|
||||
n, err := u.keyboardHidFile.Read(buf)
|
||||
if err != nil {
|
||||
u.logWithSuppression("keyboardHidFileRead", 100, &l, err, "failed to read")
|
||||
continue
|
||||
}
|
||||
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
||||
|
||||
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
|
||||
if n != 1 {
|
||||
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
||||
continue
|
||||
}
|
||||
u.updateKeyboardState(buf[0])
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (u *UsbGadget) openKeyboardHidFile() error {
|
||||
if u.keyboardHidFile != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||
}
|
||||
|
||||
if u.keyboardStateCancel != nil {
|
||||
u.keyboardStateCancel()
|
||||
}
|
||||
|
||||
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
|
||||
u.listenKeyboardEvents()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) OpenKeyboardHidFile() error {
|
||||
return u.openKeyboardHidFile()
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||
if err := u.openKeyboardHidFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := u.keyboardHidFile.Write(data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg0")
|
||||
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||
u.keyboardHidFile.Close()
|
||||
u.keyboardHidFile = nil
|
||||
return err
|
||||
}
|
||||
|
||||
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ var absoluteMouseConfig = gadgetConfigItem{
|
|||
configPath: []string{"hid.usb1"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"subclass": "0",
|
||||
"report_length": "6",
|
||||
},
|
||||
reportDesc: absoluteMouseCombinedReportDesc,
|
||||
|
@ -75,11 +75,12 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
|||
|
||||
_, err := u.absMouseHidFile.Write(data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg1")
|
||||
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
||||
u.absMouseHidFile.Close()
|
||||
u.absMouseHidFile = nil
|
||||
return err
|
||||
}
|
||||
u.resetLogSuppressionCounter("absMouseWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -65,11 +65,12 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
|||
|
||||
_, err := u.relMouseHidFile.Write(data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg2")
|
||||
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
||||
u.relMouseHidFile.Close()
|
||||
u.relMouseHidFile = nil
|
||||
return err
|
||||
}
|
||||
u.resetLogSuppressionCounter("relMouseWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
@ -59,6 +60,11 @@ type UsbGadget struct {
|
|||
relMouseHidFile *os.File
|
||||
relMouseLock sync.Mutex
|
||||
|
||||
keyboardState KeyboardState
|
||||
keyboardStateLock sync.Mutex
|
||||
keyboardStateCtx context.Context
|
||||
keyboardStateCancel context.CancelFunc
|
||||
|
||||
enabledDevices Devices
|
||||
|
||||
strictMode bool // only intended for testing for now
|
||||
|
@ -70,7 +76,12 @@ type UsbGadget struct {
|
|||
tx *UsbGadgetTransaction
|
||||
txLock sync.Mutex
|
||||
|
||||
onKeyboardStateChange *func(state KeyboardState)
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
logSuppressionCounter map[string]int
|
||||
logSuppressionLock sync.Mutex
|
||||
}
|
||||
|
||||
const configFSPath = "/sys/kernel/config"
|
||||
|
@ -96,23 +107,30 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
|||
config = &Config{isEmpty: true}
|
||||
}
|
||||
|
||||
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
|
||||
|
||||
g := &UsbGadget{
|
||||
name: name,
|
||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||
configMap: configMap,
|
||||
customConfig: *config,
|
||||
configLock: sync.Mutex{},
|
||||
keyboardLock: sync.Mutex{},
|
||||
absMouseLock: sync.Mutex{},
|
||||
relMouseLock: sync.Mutex{},
|
||||
txLock: sync.Mutex{},
|
||||
enabledDevices: *enabledDevices,
|
||||
lastUserInput: time.Now(),
|
||||
log: logger,
|
||||
name: name,
|
||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||
configMap: configMap,
|
||||
customConfig: *config,
|
||||
configLock: sync.Mutex{},
|
||||
keyboardLock: sync.Mutex{},
|
||||
absMouseLock: sync.Mutex{},
|
||||
relMouseLock: sync.Mutex{},
|
||||
txLock: sync.Mutex{},
|
||||
keyboardStateCtx: keyboardCtx,
|
||||
keyboardStateCancel: keyboardCancel,
|
||||
keyboardState: KeyboardState{},
|
||||
enabledDevices: *enabledDevices,
|
||||
lastUserInput: time.Now(),
|
||||
log: logger,
|
||||
|
||||
strictMode: config.strictMode,
|
||||
|
||||
logSuppressionCounter: make(map[string]int),
|
||||
|
||||
absMouseAccumulatedWheelY: 0,
|
||||
}
|
||||
if err := g.Init(); err != nil {
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func joinPath(basePath string, paths []string) string {
|
||||
|
@ -78,3 +80,33 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) {
|
||||
u.logSuppressionLock.Lock()
|
||||
defer u.logSuppressionLock.Unlock()
|
||||
|
||||
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||
u.logSuppressionCounter[counterName] = 0
|
||||
} else {
|
||||
u.logSuppressionCounter[counterName]++
|
||||
}
|
||||
|
||||
l := logger.With().Int("counter", u.logSuppressionCounter[counterName]).Logger()
|
||||
|
||||
if u.logSuppressionCounter[counterName]%every == 0 {
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msgf(msg, args...)
|
||||
} else {
|
||||
l.Error().Msgf(msg, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) resetLogSuppressionCounter(counterName string) {
|
||||
u.logSuppressionLock.Lock()
|
||||
defer u.logSuppressionLock.Unlock()
|
||||
|
||||
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||
u.logSuppressionCounter[counterName] = 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package websecure
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
fixtureEd25519Certificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG
|
||||
A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1
|
||||
MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV
|
||||
BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev
|
||||
bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy
|
||||
r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U
|
||||
C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB
|
||||
-----END PRIVATE KEY-----`
|
||||
|
||||
certStore *CertStore
|
||||
certSigner *SelfSigner
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
tlsStorePath, err := os.MkdirTemp("", "jktls.*")
|
||||
if err != nil {
|
||||
defaultLogger.Fatal().Err(err).Msg("failed to create temp directory")
|
||||
}
|
||||
|
||||
certStore = NewCertStore(tlsStorePath, nil)
|
||||
certStore.LoadCertificates()
|
||||
|
||||
certSigner = NewSelfSigner(
|
||||
certStore,
|
||||
nil,
|
||||
"ci.jetkvm.com",
|
||||
"JetKVM",
|
||||
"JetKVM",
|
||||
"JetKVM",
|
||||
)
|
||||
|
||||
m.Run()
|
||||
|
||||
os.RemoveAll(tlsStorePath)
|
||||
}
|
||||
|
||||
func TestSaveEd25519Certificate(t *testing.T) {
|
||||
err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to save certificate: %v", err)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package websecure
|
|||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
|
@ -37,11 +38,15 @@ func keyToFile(cert *tls.Certificate, filename string) error {
|
|||
if e != nil {
|
||||
return fmt.Errorf("failed to marshal EC private key: %v", e)
|
||||
}
|
||||
|
||||
keyBlock = pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: b,
|
||||
}
|
||||
case ed25519.PrivateKey:
|
||||
keyBlock = pem.Block{
|
||||
Type: "ED25519 PRIVATE KEY",
|
||||
Bytes: k,
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown private key type: %T", k)
|
||||
}
|
||||
|
|
|
@ -15,9 +15,7 @@ func rpcGetJigglerState() bool {
|
|||
return jigglerEnabled
|
||||
}
|
||||
|
||||
func init() {
|
||||
ensureConfigLoaded()
|
||||
|
||||
func initJiggler() {
|
||||
go runJiggler()
|
||||
}
|
||||
|
||||
|
|
86
jsonrpc.go
86
jsonrpc.go
|
@ -466,7 +466,31 @@ func rpcSetTLSState(state TLSState) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
||||
type RPCHandler struct {
|
||||
Func interface{}
|
||||
Params []string
|
||||
}
|
||||
|
||||
// call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls
|
||||
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result interface{}, err error) {
|
||||
// Use defer to recover from a panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Convert the panic to an error
|
||||
if e, ok := r.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = fmt.Errorf("panic occurred: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Call the handler
|
||||
result, err = riskyCallRPCHandler(handler, params)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
||||
handlerValue := reflect.ValueOf(handler.Func)
|
||||
handlerType := handlerValue.Type()
|
||||
|
||||
|
@ -563,11 +587,6 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac
|
|||
return nil, errors.New("unexpected return values from handler")
|
||||
}
|
||||
|
||||
type RPCHandler struct {
|
||||
Func interface{}
|
||||
Params []string
|
||||
}
|
||||
|
||||
func rpcSetMassStorageMode(mode string) (string, error) {
|
||||
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
||||
var cdrom bool
|
||||
|
@ -662,10 +681,11 @@ func rpcResetConfig() error {
|
|||
}
|
||||
|
||||
type DCPowerState struct {
|
||||
IsOn bool `json:"isOn"`
|
||||
Voltage float64 `json:"voltage"`
|
||||
Current float64 `json:"current"`
|
||||
Power float64 `json:"power"`
|
||||
IsOn bool `json:"isOn"`
|
||||
Voltage float64 `json:"voltage"`
|
||||
Current float64 `json:"current"`
|
||||
Power float64 `json:"power"`
|
||||
RestoreState int `json:"restoreState"`
|
||||
}
|
||||
|
||||
func rpcGetDCPowerState() (DCPowerState, error) {
|
||||
|
@ -681,6 +701,15 @@ func rpcSetDCPowerState(enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func rpcSetDCRestoreState(state int) error {
|
||||
logger.Info().Int("state", state).Msg("Setting DC restore state")
|
||||
err := setDCRestoreState(state)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set DC restore state: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcGetActiveExtension() (string, error) {
|
||||
return config.ActiveExtension, nil
|
||||
}
|
||||
|
@ -882,6 +911,18 @@ func rpcSetCloudUrl(apiUrl string, appUrl 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)
|
||||
|
@ -975,6 +1016,25 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func rpcGetLocalLoopbackOnly() (bool, error) {
|
||||
return config.LocalLoopbackOnly, nil
|
||||
}
|
||||
|
||||
func rpcSetLocalLoopbackOnly(enabled bool) error {
|
||||
// Check if the setting is actually changing
|
||||
if config.LocalLoopbackOnly == enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update the setting
|
||||
config.LocalLoopbackOnly = enabled
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var rpcHandlers = map[string]RPCHandler{
|
||||
"ping": {Func: rpcPing},
|
||||
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
||||
|
@ -986,6 +1046,7 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
||||
|
@ -1037,6 +1098,7 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
||||
"getDCPowerState": {Func: rpcGetDCPowerState},
|
||||
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
||||
"setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}},
|
||||
"getActiveExtension": {Func: rpcGetActiveExtension},
|
||||
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
||||
"getATXState": {Func: rpcGetATXState},
|
||||
|
@ -1047,6 +1109,10 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
||||
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
||||
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
||||
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
|
||||
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
||||
"getKeyboardMacros": {Func: getKeyboardMacros},
|
||||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
||||
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
||||
}
|
||||
|
|
6
main.go
6
main.go
|
@ -75,8 +75,8 @@ func Main() {
|
|||
}
|
||||
}()
|
||||
|
||||
// initialize usb gadget
|
||||
initUsbGadget()
|
||||
|
||||
if err := setInitialVirtualMediaState(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
|
||||
}
|
||||
|
@ -84,6 +84,10 @@ func Main() {
|
|||
if err := initImagesFolder(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to init images folder")
|
||||
}
|
||||
initJiggler()
|
||||
|
||||
// initialize display
|
||||
initDisplay()
|
||||
|
||||
go func() {
|
||||
time.Sleep(15 * time.Minute)
|
||||
|
|
106
native.go
106
native.go
|
@ -8,6 +8,7 @@ import (
|
|||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -42,6 +43,11 @@ var ongoingRequests = make(map[int32]chan *CtrlResponse)
|
|||
|
||||
var lock = &sync.Mutex{}
|
||||
|
||||
var (
|
||||
nativeCmd *exec.Cmd
|
||||
nativeCmdLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
@ -130,16 +136,26 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
|
|||
scopedLogger.Info().Msg("server listening")
|
||||
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
listener.Close()
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
||||
continue
|
||||
}
|
||||
if isCtrl {
|
||||
// check if the channel is closed
|
||||
select {
|
||||
case <-ctrlClientConnected:
|
||||
scopedLogger.Debug().Msg("ctrl client reconnected")
|
||||
default:
|
||||
close(ctrlClientConnected)
|
||||
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
||||
}
|
||||
}
|
||||
|
||||
go handleClient(conn)
|
||||
}
|
||||
if isCtrl {
|
||||
close(ctrlClientConnected)
|
||||
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
||||
}
|
||||
handleClient(conn)
|
||||
}()
|
||||
|
||||
return listener
|
||||
|
@ -236,6 +252,58 @@ func handleVideoClient(conn net.Conn) {
|
|||
}
|
||||
}
|
||||
|
||||
func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
|
||||
nativeCmdLock.Lock()
|
||||
defer nativeCmdLock.Unlock()
|
||||
|
||||
cmd, err := startNativeBinary(binaryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nativeCmd = cmd
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func restartNativeBinary(binaryPath string) error {
|
||||
time.Sleep(10 * time.Second)
|
||||
// restart the binary
|
||||
nativeLogger.Info().Msg("restarting jetkvm_native binary")
|
||||
cmd, err := startNativeBinary(binaryPath)
|
||||
if err != nil {
|
||||
nativeLogger.Warn().Err(err).Msg("failed to restart binary")
|
||||
}
|
||||
nativeCmd = cmd
|
||||
|
||||
// reset the display state
|
||||
time.Sleep(1 * time.Second)
|
||||
clearDisplayState()
|
||||
updateStaticContents()
|
||||
requestDisplayUpdate(true)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func superviseNativeBinary(binaryPath string) error {
|
||||
nativeCmdLock.Lock()
|
||||
defer nativeCmdLock.Unlock()
|
||||
|
||||
if nativeCmd == nil || nativeCmd.Process == nil {
|
||||
return restartNativeBinary(binaryPath)
|
||||
}
|
||||
|
||||
err := nativeCmd.Wait()
|
||||
|
||||
if err == nil {
|
||||
nativeLogger.Info().Err(err).Msg("jetkvm_native binary exited with no error")
|
||||
} else if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
nativeLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("jetkvm_native binary exited with error")
|
||||
} else {
|
||||
nativeLogger.Warn().Err(err).Msg("jetkvm_native binary exited with unknown error")
|
||||
}
|
||||
|
||||
return restartNativeBinary(binaryPath)
|
||||
}
|
||||
|
||||
func ExtractAndRunNativeBin() error {
|
||||
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
|
||||
if err := ensureBinaryUpdated(binaryPath); err != nil {
|
||||
|
@ -247,12 +315,28 @@ func ExtractAndRunNativeBin() error {
|
|||
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||
}
|
||||
// Run the binary in the background
|
||||
cmd, err := startNativeBinary(binaryPath)
|
||||
cmd, err := startNativeBinaryWithLock(binaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start binary: %w", err)
|
||||
}
|
||||
|
||||
//TODO: add auto restart
|
||||
// check if the binary is still running every 10 seconds
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-appCtx.Done():
|
||||
nativeLogger.Info().Msg("stopping native binary supervisor")
|
||||
return
|
||||
default:
|
||||
err := superviseNativeBinary(binaryPath)
|
||||
if err != nil {
|
||||
nativeLogger.Warn().Err(err).Msg("failed to supervise native binary")
|
||||
time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-appCtx.Done()
|
||||
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
|
||||
|
|
19
network.go
19
network.go
|
@ -19,8 +19,19 @@ func networkStateChanged() {
|
|||
// do not block the main thread
|
||||
go waitCtrlAndRequestDisplayUpdate(true)
|
||||
|
||||
if timeSync != nil {
|
||||
if networkState != nil {
|
||||
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
|
||||
}
|
||||
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
|
||||
}
|
||||
}
|
||||
|
||||
// always restart mDNS when the network state changes
|
||||
if mDNS != nil {
|
||||
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
||||
_ = mDNS.SetLocalNames([]string{
|
||||
networkState.GetHostname(),
|
||||
networkState.GetFQDN(),
|
||||
|
@ -54,14 +65,6 @@ func initNetwork() error {
|
|||
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
||||
config.NetworkConfig = networkConfig
|
||||
networkStateChanged()
|
||||
|
||||
if mDNS != nil {
|
||||
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
|
||||
_ = mDNS.SetLocalNames([]string{
|
||||
networkState.GetHostname(),
|
||||
networkState.GetFQDN(),
|
||||
}, true)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
39
serial.go
39
serial.go
|
@ -142,6 +142,7 @@ var dcState DCPowerState
|
|||
func runDCControl() {
|
||||
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
|
||||
reader := bufio.NewReader(port)
|
||||
hasRestoreFeature := false
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
|
@ -151,7 +152,13 @@ func runDCControl() {
|
|||
|
||||
// Split the line by semicolon
|
||||
parts := strings.Split(strings.TrimSpace(line), ";")
|
||||
if len(parts) != 4 {
|
||||
if len(parts) == 5 {
|
||||
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension with restore feature")
|
||||
hasRestoreFeature = true
|
||||
} else if len(parts) == 4 {
|
||||
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension without restore feature")
|
||||
hasRestoreFeature = false
|
||||
} else {
|
||||
scopedLogger.Warn().Str("line", line).Msg("Invalid line")
|
||||
continue
|
||||
}
|
||||
|
@ -163,6 +170,17 @@ func runDCControl() {
|
|||
continue
|
||||
}
|
||||
dcState.IsOn = powerState == 1
|
||||
if hasRestoreFeature {
|
||||
restoreState, err := strconv.Atoi(parts[4])
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("Invalid restore state")
|
||||
continue
|
||||
}
|
||||
dcState.RestoreState = restoreState
|
||||
} else {
|
||||
// -1 means not supported
|
||||
dcState.RestoreState = -1
|
||||
}
|
||||
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("Invalid voltage")
|
||||
|
@ -210,6 +228,25 @@ func setDCPowerState(on bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func setDCRestoreState(state int) error {
|
||||
_, err := port.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command := "RESTORE_MODE_OFF\n"
|
||||
switch state {
|
||||
case 1:
|
||||
command = "RESTORE_MODE_ON\n"
|
||||
case 2:
|
||||
command = "RESTORE_MODE_LAST_STATE\n"
|
||||
}
|
||||
_, err = port.Write([]byte(command))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultMode = &serial.Mode{
|
||||
BaudRate: 115200,
|
||||
DataBits: 8,
|
||||
|
|
24
terminal.go
24
terminal.go
|
@ -1,6 +1,7 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
|
@ -55,18 +56,23 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
|||
return
|
||||
}
|
||||
if msg.IsString {
|
||||
var size TerminalSize
|
||||
err := json.Unmarshal([]byte(msg.Data), &size)
|
||||
if err == nil {
|
||||
err = pty.Setsize(ptmx, &pty.Winsize{
|
||||
Rows: uint16(size.Rows),
|
||||
Cols: uint16(size.Cols),
|
||||
})
|
||||
maybeJson := bytes.TrimSpace(msg.Data)
|
||||
// Cheap check to see if this resembles JSON
|
||||
if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' {
|
||||
var size TerminalSize
|
||||
err := json.Unmarshal(maybeJson, &size)
|
||||
if err == nil {
|
||||
return
|
||||
err = pty.Setsize(ptmx, &pty.Winsize{
|
||||
Rows: uint16(size.Rows),
|
||||
Cols: uint16(size.Cols),
|
||||
})
|
||||
if err == nil {
|
||||
scopedLogger.Info().Int("rows", size.Rows).Int("cols", size.Cols).Msg("Set terminal size")
|
||||
return
|
||||
}
|
||||
}
|
||||
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
||||
}
|
||||
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
||||
}
|
||||
_, err := ptmx.Write(msg.Data)
|
||||
if err != nil {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"arrowParens": "avoid",
|
||||
"singleQuote": false,
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"tailwindFunctions": ["clsx"],
|
||||
"printWidth": 90
|
||||
"tailwindFunctions": ["clsx", "cx"],
|
||||
"printWidth": 90,
|
||||
"tailwindStylesheet": "./src/index.css"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,21 +19,21 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.3",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.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.3",
|
||||
"cva": "^1.0.0-beta.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"focus-trap-react": "^11.0.3",
|
||||
"framer-motion": "^12.11.4",
|
||||
"focus-trap-react": "^11.0.4",
|
||||
"framer-motion": "^12.23.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"react": "^19.1.0",
|
||||
|
@ -42,42 +42,42 @@
|
|||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-simple-keyboard": "^3.8.72",
|
||||
"react-simple-keyboard": "^3.8.89",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^2.15.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"validator": "^13.15.0",
|
||||
"validator": "^13.15.15",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/compat": "^1.3.1",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@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",
|
||||
"@types/validator": "^13.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"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": "^4.1.7",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
|
|
|
@ -262,7 +262,6 @@ export default function Actionbar({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
size="XS"
|
||||
|
|
|
@ -22,7 +22,7 @@ const AutoHeight = ({ children, ...props }: { children: React.ReactNode }) => {
|
|||
{...props}
|
||||
height={height}
|
||||
duration={300}
|
||||
contentClassName="h-fit"
|
||||
contentClassName="h-fit p-px"
|
||||
contentRef={contentDiv}
|
||||
disableDisplayNone
|
||||
>
|
||||
|
|
|
@ -12,7 +12,7 @@ const sizes = {
|
|||
|
||||
const checkboxVariants = cva({
|
||||
base: cx(
|
||||
"block rounded",
|
||||
"form-checkbox block rounded",
|
||||
|
||||
// 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",
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import { Button } from "@/components/Button";
|
||||
import Modal from "@/components/Modal";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
type Variant = "danger" | "success" | "warning" | "info";
|
||||
|
||||
|
@ -14,7 +14,7 @@ interface ConfirmDialogProps {
|
|||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
description: React.ReactNode;
|
||||
variant?: Variant;
|
||||
confirmText?: string;
|
||||
cancelText?: string | null;
|
||||
|
@ -84,8 +84,8 @@ export function ConfirmDialog({
|
|||
>
|
||||
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
|
||||
</div>
|
||||
<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">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h2 className="text-lg leading-tight font-bold text-black dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
|
@ -111,4 +111,4 @@ export function ConfirmDialog({
|
|||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
import { LuRefreshCcw } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
||||
import { NetworkState } from "@/hooks/stores";
|
||||
|
||||
export default function DhcpLeaseCard({
|
||||
networkState,
|
||||
setShowRenewLeaseConfirm,
|
||||
}: {
|
||||
networkState: NetworkState;
|
||||
setShowRenewLeaseConfirm: (show: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<GridCard>
|
||||
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
DHCP Lease Information
|
||||
</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={LuRefreshCcw}
|
||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
);
|
||||
}
|
|
@ -28,6 +28,7 @@ export default function InfoBar() {
|
|||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpcDataChannel) return;
|
||||
|
@ -36,9 +37,9 @@ export default function InfoBar() {
|
|||
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||
}, [rpcDataChannel]);
|
||||
|
||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
||||
const isNumLockActive = useHidStore(state => state.isNumLockActive);
|
||||
const isScrollLockActive = useHidStore(state => state.isScrollLockActive);
|
||||
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
||||
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||
|
||||
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
||||
|
||||
|
@ -97,19 +98,21 @@ export default function InfoBar() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">Keys:</span>
|
||||
<h2 className="text-xs">
|
||||
{[
|
||||
...activeKeys.map(
|
||||
x => Object.entries(keys).filter(y => y[1] === x)[0][0],
|
||||
),
|
||||
activeModifiers.map(
|
||||
x => Object.entries(modifiers).filter(y => y[1] === x)[0][0],
|
||||
),
|
||||
].join(", ")}
|
||||
</h2>
|
||||
</div>
|
||||
{showPressedKeys && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="text-xs font-semibold">Keys:</span>
|
||||
<h2 className="text-xs">
|
||||
{[
|
||||
...activeKeys.map(
|
||||
x => Object.entries(keys).filter(y => y[1] === x)[0][0],
|
||||
),
|
||||
activeModifiers.map(
|
||||
x => Object.entries(modifiers).filter(y => y[1] === x)[0][0],
|
||||
),
|
||||
].join(", ")}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
||||
|
@ -118,10 +121,24 @@ export default function InfoBar() {
|
|||
Relayed by Cloudflare
|
||||
</div>
|
||||
)}
|
||||
|
||||
{keyboardLedStateSyncAvailable ? (
|
||||
<div
|
||||
className={cx(
|
||||
"shrink-0 p-1 px-1.5 text-xs",
|
||||
keyboardLedSync !== "browser"
|
||||
? "text-black dark:text-white"
|
||||
: "text-slate-800/20 dark:text-slate-300/20",
|
||||
)}
|
||||
title={"Your keyboard LED state is managed by" + (keyboardLedSync === "browser" ? " the browser" : " the host")}
|
||||
>
|
||||
{keyboardLedSync === "browser" ? "Browser" : "Host"}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={cx(
|
||||
"shrink-0 p-1 px-1.5 text-xs",
|
||||
isCapsLockActive
|
||||
keyboardLedState?.caps_lock
|
||||
? "text-black dark:text-white"
|
||||
: "text-slate-800/20 dark:text-slate-300/20",
|
||||
)}
|
||||
|
@ -131,7 +148,7 @@ export default function InfoBar() {
|
|||
<div
|
||||
className={cx(
|
||||
"shrink-0 p-1 px-1.5 text-xs",
|
||||
isNumLockActive
|
||||
keyboardLedState?.num_lock
|
||||
? "text-black dark:text-white"
|
||||
: "text-slate-800/20 dark:text-slate-300/20",
|
||||
)}
|
||||
|
@ -141,13 +158,23 @@ export default function InfoBar() {
|
|||
<div
|
||||
className={cx(
|
||||
"shrink-0 p-1 px-1.5 text-xs",
|
||||
isScrollLockActive
|
||||
keyboardLedState?.scroll_lock
|
||||
? "text-black dark:text-white"
|
||||
: "text-slate-800/20 dark:text-slate-300/20",
|
||||
)}
|
||||
>
|
||||
Scroll Lock
|
||||
</div>
|
||||
{keyboardLedState?.compose ? (
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||
Compose
|
||||
</div>
|
||||
) : null}
|
||||
{keyboardLedState?.kana ? (
|
||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||
Kana
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import { NetworkState } from "../hooks/stores";
|
||||
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
|
||||
|
||||
import { GridCard } from "./Card";
|
||||
|
||||
export default function Ipv6NetworkCard({
|
||||
networkState,
|
||||
}: {
|
||||
networkState: NetworkState;
|
||||
}) {
|
||||
return (
|
||||
<GridCard>
|
||||
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Information
|
||||
</h3>
|
||||
|
||||
<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-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
|
||||
>
|
||||
<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>
|
||||
</GridCard>
|
||||
);
|
||||
}
|
|
@ -20,7 +20,9 @@ const Modal = React.memo(function Modal({
|
|||
transition
|
||||
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-500 data-leave:duration-200 data-enter:ease-out data-leave:ease-in dark:bg-slate-900/90"
|
||||
/>
|
||||
<div className="fixed inset-0 z-20 w-screen overflow-y-auto">
|
||||
<div className="fixed inset-0 z-20 w-screen overflow-y-auto" style={{
|
||||
scrollbarGutter: 'stable'
|
||||
}}>
|
||||
{/* TODO: This doesn't work well with other-sessions */}
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
|
||||
<DialogPanel
|
||||
|
|
|
@ -61,9 +61,9 @@ function Terminal({
|
|||
dataChannel,
|
||||
type,
|
||||
}: {
|
||||
title: string;
|
||||
dataChannel: RTCDataChannel;
|
||||
type: AvailableTerminalTypes;
|
||||
readonly title: string;
|
||||
readonly dataChannel: RTCDataChannel;
|
||||
readonly type: AvailableTerminalTypes;
|
||||
}) {
|
||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||
|
|
|
@ -10,7 +10,7 @@ import LoadingSpinner from "@components/LoadingSpinner";
|
|||
import Card, { GridCard } from "@components/Card";
|
||||
|
||||
interface OverlayContentProps {
|
||||
children: React.ReactNode;
|
||||
readonly children: React.ReactNode;
|
||||
}
|
||||
function OverlayContent({ children }: OverlayContentProps) {
|
||||
return (
|
||||
|
@ -23,7 +23,7 @@ function OverlayContent({ children }: OverlayContentProps) {
|
|||
}
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
show: boolean;
|
||||
readonly show: boolean;
|
||||
}
|
||||
|
||||
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||
|
@ -57,8 +57,8 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
|||
}
|
||||
|
||||
interface LoadingConnectionOverlayProps {
|
||||
show: boolean;
|
||||
text: string;
|
||||
readonly show: boolean;
|
||||
readonly text: string;
|
||||
}
|
||||
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
|
||||
return (
|
||||
|
@ -91,8 +91,8 @@ export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverla
|
|||
}
|
||||
|
||||
interface ConnectionErrorOverlayProps {
|
||||
show: boolean;
|
||||
setupPeerConnection: () => Promise<void>;
|
||||
readonly show: boolean;
|
||||
readonly setupPeerConnection: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function ConnectionFailedOverlay({
|
||||
|
@ -153,7 +153,7 @@ export function ConnectionFailedOverlay({
|
|||
}
|
||||
|
||||
interface PeerConnectionDisconnectedOverlay {
|
||||
show: boolean;
|
||||
readonly show: boolean;
|
||||
}
|
||||
|
||||
export function PeerConnectionDisconnectedOverlay({
|
||||
|
@ -207,8 +207,8 @@ export function PeerConnectionDisconnectedOverlay({
|
|||
}
|
||||
|
||||
interface HDMIErrorOverlayProps {
|
||||
show: boolean;
|
||||
hdmiState: string;
|
||||
readonly show: boolean;
|
||||
readonly hdmiState: string;
|
||||
}
|
||||
|
||||
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||
|
@ -310,8 +310,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
|||
}
|
||||
|
||||
interface NoAutoplayPermissionsOverlayProps {
|
||||
show: boolean;
|
||||
onPlayClick: () => void;
|
||||
readonly show: boolean;
|
||||
readonly onPlayClick: () => void;
|
||||
}
|
||||
|
||||
export function NoAutoplayPermissionsOverlay({
|
||||
|
@ -361,7 +361,7 @@ export function NoAutoplayPermissionsOverlay({
|
|||
}
|
||||
|
||||
interface PointerLockBarProps {
|
||||
show: boolean;
|
||||
readonly show: boolean;
|
||||
}
|
||||
|
||||
export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||
|
@ -369,10 +369,10 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
|
|||
<AnimatePresence mode="wait">
|
||||
{show ? (
|
||||
<motion.div
|
||||
className="absolute -top-[36px] left-0 right-0 z-20 bg-white"
|
||||
initial={{ y: 20, opacity: 0, zIndex: 0 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ y: 43, zIndex: 0 }}
|
||||
className="flex w-full items-center justify-between bg-transparent"
|
||||
initial={{ opacity: 0, zIndex: 0 }}
|
||||
animate={{ opacity: 1, zIndex: 20 }}
|
||||
exit={{ opacity: 0, zIndex: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
|
||||
>
|
||||
<div>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Keyboard from "react-simple-keyboard";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Keyboard from "react-simple-keyboard";
|
||||
|
||||
import Card from "@components/Card";
|
||||
// eslint-disable-next-line import/order
|
||||
|
@ -9,12 +10,12 @@ import { Button } from "@components/Button";
|
|||
|
||||
import "react-simple-keyboard/build/css/index.css";
|
||||
|
||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
||||
import { cx } from "@/cva.config";
|
||||
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||
import { cx } from "@/cva.config";
|
||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
|
||||
|
||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||
|
@ -40,7 +41,17 @@ function KeyboardWrapper() {
|
|||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
||||
|
||||
const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));
|
||||
|
||||
// HID related states
|
||||
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||
);
|
||||
|
||||
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||
|
||||
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
|
@ -157,14 +168,16 @@ function KeyboardWrapper() {
|
|||
toggleLayout();
|
||||
|
||||
if (isCapsLockActive) {
|
||||
setIsCapsLockActive(false);
|
||||
if (!isKeyboardLedManagedByHost) {
|
||||
setIsCapsLockActive(false);
|
||||
}
|
||||
sendKeyboardEvent([keys["CapsLock"]], []);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle caps lock state change
|
||||
if (isKeyCaps) {
|
||||
if (isKeyCaps && !isKeyboardLedManagedByHost) {
|
||||
setIsCapsLockActive(!isCapsLockActive);
|
||||
}
|
||||
|
||||
|
@ -183,7 +196,7 @@ function KeyboardWrapper() {
|
|||
|
||||
setTimeout(resetKeyboardState, 100);
|
||||
},
|
||||
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
||||
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
||||
);
|
||||
|
||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
|
||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||
import Actionbar from "@components/ActionBar";
|
||||
import MacroBar from "@/components/MacroBar";
|
||||
import InfoBar from "@components/InfoBar";
|
||||
import notifications from "@/notifications";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { cx } from "@/cva.config";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import {
|
||||
useHidStore,
|
||||
useMouseStore,
|
||||
useRTCStore,
|
||||
useSettingsStore,
|
||||
useUiStore,
|
||||
useVideoStore,
|
||||
} from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { cx } from "@/cva.config";
|
||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||
import Actionbar from "@components/ActionBar";
|
||||
import MacroBar from "@/components/MacroBar";
|
||||
import InfoBar from "@components/InfoBar";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
import {
|
||||
HDMIErrorOverlay,
|
||||
|
@ -47,6 +46,23 @@ export default function WebRTCVideo() {
|
|||
clientHeight: videoClientHeight,
|
||||
} = useVideoStore();
|
||||
|
||||
// Video enhancement settings
|
||||
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
||||
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
||||
const videoContrast = useSettingsStore(state => state.videoContrast);
|
||||
|
||||
// HID related states
|
||||
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||
);
|
||||
|
||||
const setIsNumLockActive = useHidStore(state => state.setIsNumLockActive);
|
||||
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||
const setIsScrollLockActive = useHidStore(state => state.setIsScrollLockActive);
|
||||
|
||||
// RTC related states
|
||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
||||
|
||||
|
@ -55,12 +71,9 @@ export default function WebRTCVideo() {
|
|||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||
const isVideoLoading = !isPlaying;
|
||||
|
||||
// Keyboard related states
|
||||
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } =
|
||||
useHidStore();
|
||||
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||
|
||||
// Misc states and hooks
|
||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
// Video-related
|
||||
|
@ -98,32 +111,77 @@ export default function WebRTCVideo() {
|
|||
);
|
||||
|
||||
// Pointer lock and keyboard lock related
|
||||
const isPointerLockPossible = window.location.protocol === "https:";
|
||||
|
||||
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
||||
const isFullscreenEnabled = document.fullscreenEnabled;
|
||||
|
||||
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
||||
const name = permissionName as PermissionName;
|
||||
const { state } = await navigator.permissions.query({ name });
|
||||
return state === "granted";
|
||||
if (!navigator.permissions || !navigator.permissions.query) {
|
||||
return false; // if can't query permissions, assume NOT granted
|
||||
}
|
||||
|
||||
try {
|
||||
const name = permissionName as PermissionName;
|
||||
const { state } = await navigator.permissions.query({ name });
|
||||
return state === "granted";
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
return false; // if query fails, assume NOT granted
|
||||
}, []);
|
||||
|
||||
const requestPointerLock = useCallback(async () => {
|
||||
if (document.pointerLockElement) return;
|
||||
if (!isPointerLockPossible
|
||||
|| videoElm.current === null
|
||||
|| document.pointerLockElement) return;
|
||||
|
||||
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
||||
|
||||
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
||||
videoElm.current?.requestPointerLock();
|
||||
try {
|
||||
await videoElm.current.requestPointerLock();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}, [checkNavigatorPermissions, settings.mouseMode]);
|
||||
}, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]);
|
||||
|
||||
const requestKeyboardLock = useCallback(async () => {
|
||||
if (videoElm.current === null) return;
|
||||
|
||||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||
|
||||
if (isKeyboardLockGranted && "keyboard" in navigator) {
|
||||
try {
|
||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||
await navigator.keyboard.lock();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}, [checkNavigatorPermissions]);
|
||||
|
||||
const releaseKeyboardLock = useCallback(async () => {
|
||||
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
|
||||
|
||||
if ("keyboard" in navigator) {
|
||||
try {
|
||||
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
||||
await navigator.keyboard.unlock();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPointerLockPossible || !videoElm.current) return;
|
||||
|
||||
const handlePointerLockChange = () => {
|
||||
if (document.pointerLockElement) {
|
||||
notifications.success("Pointer lock Enabled, hold escape to exit");
|
||||
notifications.success("Pointer lock Enabled, press escape to unlock");
|
||||
setIsPointerLockActive(true);
|
||||
} else {
|
||||
notifications.success("Pointer lock disabled");
|
||||
notifications.success("Pointer lock Disabled");
|
||||
setIsPointerLockActive(false);
|
||||
}
|
||||
};
|
||||
|
@ -136,27 +194,39 @@ export default function WebRTCVideo() {
|
|||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [isPointerLockPossible, videoElm]);
|
||||
}, [isPointerLockPossible]);
|
||||
|
||||
const requestFullscreen = useCallback(async () => {
|
||||
videoElm.current?.requestFullscreen({
|
||||
navigationUI: "show",
|
||||
});
|
||||
if (!isFullscreenEnabled || !videoElm.current) return;
|
||||
|
||||
// we do not care about pointer lock if it's for fullscreen
|
||||
// per https://wicg.github.io/keyboard-lock/#system-key-press-handler
|
||||
// If keyboard lock is activated after fullscreen is already in effect, then the user my
|
||||
// see multiple messages about how to exit fullscreen. For this reason, we recommend that
|
||||
// developers call lock() before they enter fullscreen:
|
||||
await requestKeyboardLock();
|
||||
await requestPointerLock();
|
||||
|
||||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||
if (isKeyboardLockGranted) {
|
||||
if ("keyboard" in navigator) {
|
||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||
await navigator.keyboard.lock();
|
||||
await videoElm.current.requestFullscreen({
|
||||
navigationUI: "show",
|
||||
});
|
||||
}, [isFullscreenEnabled, requestKeyboardLock, requestPointerLock]);
|
||||
|
||||
// setup to release the keyboard lock anytime the fullscreen ends
|
||||
useEffect(() => {
|
||||
if (!videoElm.current) return;
|
||||
|
||||
const handleFullscreenChange = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
releaseKeyboardLock();
|
||||
}
|
||||
}
|
||||
}, [requestPointerLock, checkNavigatorPermissions]);
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange ", handleFullscreenChange);
|
||||
}, [releaseKeyboardLock]);
|
||||
|
||||
// Mouse-related
|
||||
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
||||
|
||||
const sendRelMouseMovement = useCallback(
|
||||
(x: number, y: number, buttons: number) => {
|
||||
if (settings.mouseMode !== "relative") return;
|
||||
|
@ -171,18 +241,13 @@ export default function WebRTCVideo() {
|
|||
const relMouseMoveHandler = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (settings.mouseMode !== "relative") return;
|
||||
if (isPointerLockActive === false && isPointerLockPossible === true) return;
|
||||
if (isPointerLockActive === false && isPointerLockPossible) return;
|
||||
|
||||
// Send mouse movement
|
||||
const { buttons } = e;
|
||||
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
||||
},
|
||||
[
|
||||
isPointerLockActive,
|
||||
isPointerLockPossible,
|
||||
sendRelMouseMovement,
|
||||
settings.mouseMode,
|
||||
],
|
||||
[isPointerLockActive, isPointerLockPossible, sendRelMouseMovement, settings.mouseMode],
|
||||
);
|
||||
|
||||
const sendAbsMouseMovement = useCallback(
|
||||
|
@ -236,18 +301,16 @@ export default function WebRTCVideo() {
|
|||
const { buttons } = e;
|
||||
sendAbsMouseMovement(x, y, buttons);
|
||||
},
|
||||
[
|
||||
sendAbsMouseMovement,
|
||||
videoClientHeight,
|
||||
videoClientWidth,
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
settings.mouseMode,
|
||||
],
|
||||
[settings.mouseMode, videoClientWidth, videoClientHeight, videoWidth, videoHeight, sendAbsMouseMovement],
|
||||
);
|
||||
|
||||
const mouseWheelHandler = useCallback(
|
||||
(e: WheelEvent) => {
|
||||
|
||||
if (settings.scrollThrottling && blockWheelEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if the wheel event is an accel scroll value
|
||||
const isAccel = Math.abs(e.deltaY) >= 100;
|
||||
|
||||
|
@ -255,7 +318,7 @@ export default function WebRTCVideo() {
|
|||
const accelScrollValue = e.deltaY / 100;
|
||||
|
||||
// Calculate the no accel scroll value
|
||||
const noAccelScrollValue = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
|
||||
const noAccelScrollValue = Math.sign(e.deltaY);
|
||||
|
||||
// Get scroll value
|
||||
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
||||
|
@ -267,8 +330,14 @@ export default function WebRTCVideo() {
|
|||
const invertedScrollValue = -clampedScrollValue;
|
||||
|
||||
send("wheelReport", { wheelY: invertedScrollValue });
|
||||
|
||||
// Apply blocking delay based of throttling settings
|
||||
if (settings.scrollThrottling && !blockWheelEvent) {
|
||||
setBlockWheelEvent(true);
|
||||
setTimeout(() => setBlockWheelEvent(false), settings.scrollThrottling);
|
||||
}
|
||||
},
|
||||
[send],
|
||||
[send, blockWheelEvent, settings],
|
||||
);
|
||||
|
||||
const resetMousePosition = useCallback(() => {
|
||||
|
@ -348,16 +417,11 @@ export default function WebRTCVideo() {
|
|||
let code = e.code;
|
||||
const key = e.key;
|
||||
|
||||
// if (document.activeElement?.id !== "videoFocusTrap") {
|
||||
// console.log("KEYUP: Not focusing on the video", document.activeElement);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// console.log(document.activeElement);
|
||||
|
||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||
if (!isKeyboardLedManagedByHost) {
|
||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||
}
|
||||
|
||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||
code = "Backquote";
|
||||
|
@ -388,11 +452,12 @@ export default function WebRTCVideo() {
|
|||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||
},
|
||||
[
|
||||
handleModifierKeys,
|
||||
sendKeyboardEvent,
|
||||
isKeyboardLedManagedByHost,
|
||||
setIsNumLockActive,
|
||||
setIsCapsLockActive,
|
||||
setIsScrollLockActive,
|
||||
handleModifierKeys,
|
||||
sendKeyboardEvent,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -401,9 +466,11 @@ export default function WebRTCVideo() {
|
|||
e.preventDefault();
|
||||
const prev = useHidStore.getState();
|
||||
|
||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||
if (!isKeyboardLedManagedByHost) {
|
||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||
}
|
||||
|
||||
// Filtering out the key that was just released (keys[e.code])
|
||||
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
||||
|
@ -417,22 +484,25 @@ export default function WebRTCVideo() {
|
|||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||
},
|
||||
[
|
||||
handleModifierKeys,
|
||||
sendKeyboardEvent,
|
||||
isKeyboardLedManagedByHost,
|
||||
setIsNumLockActive,
|
||||
setIsCapsLockActive,
|
||||
setIsScrollLockActive,
|
||||
handleModifierKeys,
|
||||
sendKeyboardEvent,
|
||||
],
|
||||
);
|
||||
|
||||
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
||||
if (!videoElm.current) return;
|
||||
|
||||
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
||||
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
||||
// Fix only works in chrome based browsers.
|
||||
if (e.code === "Space") {
|
||||
if (videoElm.current?.paused == true) {
|
||||
if (videoElm.current.paused) {
|
||||
console.log("Force playing video");
|
||||
videoElm.current?.play();
|
||||
videoElm.current.play();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
@ -441,7 +511,6 @@ export default function WebRTCVideo() {
|
|||
(mediaStream: MediaStream) => {
|
||||
if (!videoElm.current) return;
|
||||
const videoElmRefValue = videoElm.current;
|
||||
// console.log("Adding stream to video element", videoElmRefValue);
|
||||
videoElmRefValue.srcObject = mediaStream;
|
||||
updateVideoSizeStore(videoElmRefValue);
|
||||
},
|
||||
|
@ -457,7 +526,6 @@ export default function WebRTCVideo() {
|
|||
peerConnection.addEventListener(
|
||||
"track",
|
||||
(e: RTCTrackEvent) => {
|
||||
// console.log("Adding stream to video element");
|
||||
addStreamToVideoElm(e.streams[0]);
|
||||
},
|
||||
{ signal },
|
||||
|
@ -473,7 +541,6 @@ export default function WebRTCVideo() {
|
|||
useEffect(
|
||||
function updateVideoStream() {
|
||||
if (!mediaStream) return;
|
||||
console.log("Updating video stream from mediaStream");
|
||||
// We set the as early as possible
|
||||
addStreamToVideoElm(mediaStream);
|
||||
},
|
||||
|
@ -495,9 +562,6 @@ export default function WebRTCVideo() {
|
|||
document.addEventListener("keydown", keyDownHandler, { signal });
|
||||
document.addEventListener("keyup", keyUpHandler, { signal });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
window.clearKeys = () => sendKeyboardEvent([], []);
|
||||
window.addEventListener("blur", resetKeyboardState, { signal });
|
||||
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
|
||||
|
||||
|
@ -505,7 +569,7 @@ export default function WebRTCVideo() {
|
|||
abortController.abort();
|
||||
};
|
||||
},
|
||||
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
|
||||
[keyDownHandler, keyUpHandler, resetKeyboardState],
|
||||
);
|
||||
|
||||
// Setup Video Event Listeners
|
||||
|
@ -527,38 +591,42 @@ export default function WebRTCVideo() {
|
|||
abortController.abort();
|
||||
};
|
||||
},
|
||||
[
|
||||
absMouseMoveHandler,
|
||||
resetMousePosition,
|
||||
onVideoPlaying,
|
||||
mouseWheelHandler,
|
||||
videoKeyUpHandler,
|
||||
],
|
||||
[onVideoPlaying, videoKeyUpHandler],
|
||||
);
|
||||
|
||||
// Setup Absolute Mouse Events
|
||||
// Setup Mouse Events
|
||||
useEffect(
|
||||
function setAbsoluteMouseModeEventListeners() {
|
||||
function setMouseModeEventListeners() {
|
||||
const videoElmRefValue = videoElm.current;
|
||||
if (!videoElmRefValue) return;
|
||||
|
||||
if (settings.mouseMode !== "absolute") return;
|
||||
const isRelativeMouseMode = (settings.mouseMode === "relative");
|
||||
|
||||
const abortController = new AbortController();
|
||||
const signal = abortController.signal;
|
||||
|
||||
videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler :absMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||
signal,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
// Reset the mouse position when the window is blurred or the document is hidden
|
||||
const local = resetMousePosition;
|
||||
window.addEventListener("blur", local, { signal });
|
||||
document.addEventListener("visibilitychange", local, { signal });
|
||||
if (isRelativeMouseMode) {
|
||||
videoElmRefValue.addEventListener("click",
|
||||
() => {
|
||||
if (isPointerLockPossible && !isPointerLockActive && !document.pointerLockElement) {
|
||||
requestPointerLock();
|
||||
}
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
} else {
|
||||
// Reset the mouse position when the window is blurred or the document is hidden
|
||||
window.addEventListener("blur", resetMousePosition, { signal });
|
||||
document.addEventListener("visibilitychange", resetMousePosition, { signal });
|
||||
}
|
||||
|
||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
||||
|
||||
|
@ -566,65 +634,18 @@ export default function WebRTCVideo() {
|
|||
abortController.abort();
|
||||
};
|
||||
},
|
||||
[absMouseMoveHandler, mouseWheelHandler, resetMousePosition, settings.mouseMode],
|
||||
[absMouseMoveHandler, isPointerLockActive, isPointerLockPossible, mouseWheelHandler, relMouseMoveHandler, requestPointerLock, resetMousePosition, settings.mouseMode],
|
||||
);
|
||||
|
||||
// Setup Relative Mouse Events
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(
|
||||
function setupRelativeMouseEventListeners() {
|
||||
if (settings.mouseMode !== "relative") return;
|
||||
// Relative mouse mode should only be active if the pointer lock is active and Pointer Lock is possible
|
||||
|
||||
const videoElmRefValue = videoElm.current;
|
||||
if (!videoElmRefValue) return;
|
||||
|
||||
const abortController = new AbortController();
|
||||
const signal = abortController.signal;
|
||||
|
||||
videoElmRefValue.addEventListener("mousemove", relMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("pointerdown", relMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener("pointerup", relMouseMoveHandler, { signal });
|
||||
videoElmRefValue.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
if (isPointerLockPossible && !document.pointerLockElement) {
|
||||
requestPointerLock();
|
||||
}
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||
signal,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
},
|
||||
[
|
||||
settings.mouseMode,
|
||||
relMouseMoveHandler,
|
||||
mouseWheelHandler,
|
||||
disableVideoFocusTrap,
|
||||
requestPointerLock,
|
||||
isPointerLockPossible,
|
||||
isPointerLockActive,
|
||||
],
|
||||
);
|
||||
|
||||
const hasNoAutoPlayPermissions = useMemo(() => {
|
||||
if (peerConnection?.connectionState !== "connected") return false;
|
||||
if (isPlaying) return false;
|
||||
if (hdmiError) return false;
|
||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||
return true;
|
||||
}, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
|
||||
}, [hdmiError, isPlaying, peerConnection?.connectionState, videoHeight, videoWidth]);
|
||||
|
||||
const showPointerLockBar = useMemo(() => {
|
||||
if (settings.mouseMode !== "relative") return false;
|
||||
|
@ -634,15 +655,17 @@ export default function WebRTCVideo() {
|
|||
if (!isPlaying) return false;
|
||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||
return true;
|
||||
}, [
|
||||
settings.mouseMode,
|
||||
isPointerLockPossible,
|
||||
isPointerLockActive,
|
||||
isVideoLoading,
|
||||
isPlaying,
|
||||
videoHeight,
|
||||
videoWidth,
|
||||
]);
|
||||
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
|
||||
|
||||
// Conditionally set the filter style so we don't fallback to software rendering if these values are default of 1.0
|
||||
const videoStyle = useMemo(() => {
|
||||
const isDefault = videoSaturation === 1.0 && videoBrightness === 1.0 && videoContrast === 1.0;
|
||||
return isDefault
|
||||
? {} // No filter if all settings are default (1.0)
|
||||
: {
|
||||
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
||||
};
|
||||
}, [videoSaturation, videoBrightness, videoContrast]);
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||
|
@ -672,23 +695,23 @@ export default function WebRTCVideo() {
|
|||
<div className="relative grow overflow-hidden">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="grid grow grid-rows-(--grid-bodyFooter) overflow-hidden">
|
||||
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
||||
<PointerLockBar show={showPointerLockBar} />
|
||||
<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">
|
||||
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
||||
<PointerLockBar show={showPointerLockBar} />
|
||||
<video
|
||||
ref={videoElm}
|
||||
autoPlay={true}
|
||||
autoPlay
|
||||
controls={false}
|
||||
onPlaying={onVideoPlaying}
|
||||
onPlay={onVideoPlaying}
|
||||
muted={true}
|
||||
muted
|
||||
playsInline
|
||||
disablePictureInPicture
|
||||
controlsList="nofullscreen"
|
||||
style={videoStyle}
|
||||
className={cx(
|
||||
"z-30 max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
||||
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
||||
{
|
||||
"cursor-none": settings.isCursorHidden,
|
||||
"opacity-0":
|
||||
|
@ -718,7 +741,6 @@ export default function WebRTCVideo() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VirtualKeyboard />
|
||||
|
|
|
@ -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">
|
||||
<Card className="h-[120px] animate-fadeIn opacity-0">
|
||||
<div className="space-y-4 p-3">
|
||||
{/* Control Buttons */}
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
|
@ -8,12 +8,14 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|||
import notifications from "@/notifications";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||
|
||||
interface DCPowerState {
|
||||
isOn: boolean;
|
||||
voltage: number;
|
||||
current: number;
|
||||
power: number;
|
||||
restoreState: number;
|
||||
}
|
||||
|
||||
export function DCPowerControl() {
|
||||
|
@ -43,6 +45,20 @@ export function DCPowerControl() {
|
|||
getDCPowerState(); // Refresh state after change
|
||||
});
|
||||
};
|
||||
const handleRestoreChange = (state: number) => {
|
||||
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
|
||||
send("setDCRestoreState", { state }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
getDCPowerState(); // Refresh state after change
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getDCPowerState();
|
||||
|
@ -63,7 +79,7 @@ export function DCPowerControl() {
|
|||
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="h-[160px] animate-fadeIn">
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<div className="space-y-4 p-3">
|
||||
{/* Power Controls */}
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -84,6 +100,21 @@ export function DCPowerControl() {
|
|||
onClick={() => handlePowerToggle(false)}
|
||||
/>
|
||||
</div>
|
||||
{powerState.restoreState > -1 ? (
|
||||
<div className="flex items-center">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label="Restore Power Loss"
|
||||
value={powerState.restoreState}
|
||||
onChange={e => handleRestoreChange(parseInt(e.target.value))}
|
||||
options={[
|
||||
{ value: '0', label: "Power OFF" },
|
||||
{ value: '1', label: "Power ON" },
|
||||
{ value: '2', label: "Last State" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
||||
|
||||
{/* Status Display */}
|
||||
|
|
|
@ -58,7 +58,7 @@ export function SerialConsole() {
|
|||
description="Configure your serial console settings"
|
||||
/>
|
||||
|
||||
<Card className="animate-fadeIn">
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<div className="space-y-4 p-3">
|
||||
{/* Open Console Button */}
|
||||
<div className="flex items-center">
|
||||
|
|
|
@ -92,7 +92,7 @@ export default function ExtensionPopover() {
|
|||
{renderActiveExtension()}
|
||||
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
||||
className="flex animate-fadeIn opacity-0 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">
|
||||
<Card className="animate-fadeIn opacity-0" >
|
||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||
{AVAILABLE_EXTENSIONS.map(extension => (
|
||||
<div
|
||||
|
|
|
@ -214,7 +214,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
|||
) : null}
|
||||
|
||||
<div
|
||||
className="animate-fadeIn space-y-2"
|
||||
className="animate-fadeIn opacity-0 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"
|
||||
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { LuCornerDownLeft } from "react-icons/lu";
|
||||
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||
import { useClose } from "@headlessui/react";
|
||||
|
@ -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, useSettingsStore } 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 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,25 @@ export default function PasteModal() {
|
|||
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
||||
const close = useClose();
|
||||
|
||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||
const setKeyboardLayout = useSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
|
||||
// this ensures we always get the original en_US if it hasn't been set yet
|
||||
const safeKeyboardLayout = useMemo(() => {
|
||||
if (keyboardLayout && keyboardLayout.length > 0)
|
||||
return keyboardLayout;
|
||||
return "en_US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
send("getKeyboardLayout", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, [send, setKeyboardLayout]);
|
||||
|
||||
const onCancelPasteMode = useCallback(() => {
|
||||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
@ -37,33 +63,48 @@ export default function PasteModal() {
|
|||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||
|
||||
if (!safeKeyboardLayout) return;
|
||||
if (!chars[safeKeyboardLayout]) return;
|
||||
const text = TextAreaRef.current.value;
|
||||
|
||||
try {
|
||||
for (const char of text) {
|
||||
const { key, shift } = chars[char] ?? {};
|
||||
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][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]),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notifications.error("Failed to paste text");
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]);
|
||||
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (TextAreaRef.current) {
|
||||
|
@ -83,14 +124,14 @@ export default function PasteModal() {
|
|||
/>
|
||||
|
||||
<div
|
||||
className="animate-fadeIn space-y-2"
|
||||
className="animate-fadeIn opacity-0 space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="w-full" onKeyUp={e => e.stopPropagation()}>
|
||||
<div className="w-full" onKeyUp={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()}>
|
||||
<TextAreaWithLabel
|
||||
ref={TextAreaRef}
|
||||
label="Paste from host"
|
||||
|
@ -113,7 +154,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[safeKeyboardLayout][char]),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -132,12 +173,17 @@ export default function PasteModal() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex animate-fadeIn items-center justify-end gap-x-2"
|
||||
className="flex animate-fadeIn opacity-0 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"
|
||||
className="animate-fadeIn opacity-0 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"
|
||||
className="flex animate-fadeIn opacity-0 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">
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<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"
|
||||
className="flex animate-fadeIn opacity-0 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">
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<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"
|
||||
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
|
|
@ -283,6 +283,8 @@ export const useVideoStore = create<VideoState>(set => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export type KeyboardLedSync = "auto" | "browser" | "host";
|
||||
|
||||
interface SettingsState {
|
||||
isCursorHidden: boolean;
|
||||
setCursorVisibility: (enabled: boolean) => void;
|
||||
|
@ -302,6 +304,26 @@ interface SettingsState {
|
|||
|
||||
backlightSettings: BacklightSettings;
|
||||
setBacklightSettings: (settings: BacklightSettings) => void;
|
||||
|
||||
keyboardLayout: string;
|
||||
setKeyboardLayout: (layout: string) => void;
|
||||
|
||||
keyboardLedSync: KeyboardLedSync;
|
||||
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
||||
|
||||
scrollThrottling: number;
|
||||
setScrollThrottling: (value: number) => void;
|
||||
|
||||
showPressedKeys: boolean;
|
||||
setShowPressedKeys: (show: boolean) => void;
|
||||
|
||||
// Video enhancement settings
|
||||
videoSaturation: number;
|
||||
setVideoSaturation: (value: number) => void;
|
||||
videoBrightness: number;
|
||||
setVideoBrightness: (value: number) => void;
|
||||
videoContrast: number;
|
||||
setVideoContrast: (value: number) => void;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create(
|
||||
|
@ -330,6 +352,26 @@ export const useSettingsStore = create(
|
|||
},
|
||||
setBacklightSettings: (settings: BacklightSettings) =>
|
||||
set({ backlightSettings: settings }),
|
||||
|
||||
keyboardLayout: "en-US",
|
||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||
|
||||
keyboardLedSync: "auto",
|
||||
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
||||
|
||||
scrollThrottling: 0,
|
||||
setScrollThrottling: value => set({ scrollThrottling: value }),
|
||||
|
||||
showPressedKeys: true,
|
||||
setShowPressedKeys: show => set({ showPressedKeys: show }),
|
||||
|
||||
// Video enhancement settings with default values (1.0 = normal)
|
||||
videoSaturation: 1.0,
|
||||
setVideoSaturation: value => set({ videoSaturation: value }),
|
||||
videoBrightness: 1.0,
|
||||
setVideoBrightness: value => set({ videoBrightness: value }),
|
||||
videoContrast: 1.0,
|
||||
setVideoContrast: value => set({ videoContrast: value }),
|
||||
}),
|
||||
{
|
||||
name: "settings",
|
||||
|
@ -338,17 +380,6 @@ export const useSettingsStore = create(
|
|||
),
|
||||
);
|
||||
|
||||
export interface DeviceSettingsState {
|
||||
trackpadSensitivity: number;
|
||||
mouseSensitivity: number;
|
||||
clampMin: number;
|
||||
clampMax: number;
|
||||
blockDelay: number;
|
||||
trackpadThreshold: number;
|
||||
scrollSensitivity: "low" | "default" | "high";
|
||||
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void;
|
||||
}
|
||||
|
||||
export interface RemoteVirtualMediaState {
|
||||
source: "WebRTC" | "HTTP" | "Storage" | null;
|
||||
mode: "CDROM" | "Disk" | null;
|
||||
|
@ -399,6 +430,21 @@ export const useMountMediaStore = create<MountMediaState>(set => ({
|
|||
setErrorMessage: message => set({ errorMessage: message }),
|
||||
}));
|
||||
|
||||
export interface KeyboardLedState {
|
||||
num_lock: boolean;
|
||||
caps_lock: boolean;
|
||||
scroll_lock: boolean;
|
||||
compose: boolean;
|
||||
kana: boolean;
|
||||
};
|
||||
const defaultKeyboardLedState: KeyboardLedState = {
|
||||
num_lock: false,
|
||||
caps_lock: false,
|
||||
scroll_lock: false,
|
||||
compose: false,
|
||||
kana: false,
|
||||
};
|
||||
|
||||
export interface HidState {
|
||||
activeKeys: number[];
|
||||
activeModifiers: number[];
|
||||
|
@ -417,18 +463,18 @@ export interface HidState {
|
|||
altGrCtrlTime: number; // _altGrCtrlTime
|
||||
setAltGrCtrlTime: (time: number) => void;
|
||||
|
||||
isNumLockActive: boolean;
|
||||
setIsNumLockActive: (enabled: boolean) => void;
|
||||
keyboardLedState?: KeyboardLedState;
|
||||
setKeyboardLedState: (state: KeyboardLedState) => void;
|
||||
setIsNumLockActive: (active: boolean) => void;
|
||||
setIsCapsLockActive: (active: boolean) => void;
|
||||
setIsScrollLockActive: (active: boolean) => void;
|
||||
|
||||
isScrollLockActive: boolean;
|
||||
setIsScrollLockActive: (enabled: boolean) => void;
|
||||
keyboardLedStateSyncAvailable: boolean;
|
||||
setKeyboardLedStateSyncAvailable: (available: boolean) => void;
|
||||
|
||||
isVirtualKeyboardEnabled: boolean;
|
||||
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
||||
|
||||
isCapsLockActive: boolean;
|
||||
setIsCapsLockActive: (enabled: boolean) => void;
|
||||
|
||||
isPasteModeEnabled: boolean;
|
||||
setPasteModeEnabled: (enabled: boolean) => void;
|
||||
|
||||
|
@ -436,7 +482,7 @@ export interface HidState {
|
|||
setUsbState: (state: HidState["usbState"]) => void;
|
||||
}
|
||||
|
||||
export const useHidStore = create<HidState>(set => ({
|
||||
export const useHidStore = create<HidState>((set, get) => ({
|
||||
activeKeys: [],
|
||||
activeModifiers: [],
|
||||
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
||||
|
@ -452,18 +498,29 @@ export const useHidStore = create<HidState>(set => ({
|
|||
altGrCtrlTime: 0,
|
||||
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
||||
|
||||
isNumLockActive: false,
|
||||
setIsNumLockActive: enabled => set({ isNumLockActive: enabled }),
|
||||
setKeyboardLedState: ledState => set({ keyboardLedState: ledState }),
|
||||
setIsNumLockActive: active => {
|
||||
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||
keyboardLedState.num_lock = active;
|
||||
set({ keyboardLedState });
|
||||
},
|
||||
setIsCapsLockActive: active => {
|
||||
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||
keyboardLedState.caps_lock = active;
|
||||
set({ keyboardLedState });
|
||||
},
|
||||
setIsScrollLockActive: active => {
|
||||
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||
keyboardLedState.scroll_lock = active;
|
||||
set({ keyboardLedState });
|
||||
},
|
||||
|
||||
isScrollLockActive: false,
|
||||
setIsScrollLockActive: enabled => set({ isScrollLockActive: enabled }),
|
||||
keyboardLedStateSyncAvailable: false,
|
||||
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }),
|
||||
|
||||
isVirtualKeyboardEnabled: false,
|
||||
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
||||
|
||||
isCapsLockActive: false,
|
||||
setIsCapsLockActive: enabled => set({ isCapsLockActive: enabled }),
|
||||
|
||||
isPasteModeEnabled: false,
|
||||
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import 'tailwindcss';
|
||||
@import "tailwindcss";
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
|
@ -10,10 +10,10 @@
|
|||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Circular', sans-serif;
|
||||
--font-display: 'Circular', sans-serif;
|
||||
--font-serif: 'Circular', serif;
|
||||
--font-mono: 'Source Code Pro Variable', monospace;
|
||||
--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;
|
||||
|
@ -122,13 +122,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||
/* prettier-ignore */
|
||||
@utility max-width-* {
|
||||
max-width: --modifier(--container- *, [length], [ *]);
|
||||
max-width: --modifier(--container-*, [length], [*]);
|
||||
}
|
||||
|
||||
/* Ensure there is not a `ms` and ms -> `...)ms` */
|
||||
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||
/* prettier-ignore */
|
||||
@utility animation-delay-* {
|
||||
animation-delay: --value(integer) ms;
|
||||
animation-delay: --value(integer)ms;
|
||||
}
|
||||
|
||||
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||
/* prettier-ignore */
|
||||
@utility animation-duration-* {
|
||||
animation-duration: --value(integer)ms;
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
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"
|
||||
|
||||
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
|
||||
export const layouts: Record<string, string> = {
|
||||
be_FR: name_fr_BE,
|
||||
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>> = {
|
||||
be_FR: chars_fr_BE,
|
||||
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,167 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Belgisch Nederlands";
|
||||
|
||||
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
|
||||
const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Slash", altRight: true } // tilde, 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 },
|
||||
"Á": { key: "KeyQ", shift: true, accentKey: keyAcute },
|
||||
"À": { key: "KeyQ", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyQ", 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: keyHat },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"È": { 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: keyHat },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Ì": { 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: "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 },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
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 },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Ù": { 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: "KeyQ" },
|
||||
"ä": { key: "KeyQ", accentKey: keyTrema },
|
||||
"â": { key: "KeyQ", accentKey: keyHat },
|
||||
"á": { key: "KeyQ", accentKey: keyAcute },
|
||||
"ã": { key: "KeyQ", 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: keyHat },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
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: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
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 },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"ũ": { 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 },
|
||||
"&": { key: "Digit1" },
|
||||
1: { key: "Digit1", shift: true },
|
||||
"|": { key: "Digit1", altRight: 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: "Digit5" },
|
||||
5: { key: "Digit5", shift: true },
|
||||
"§": { key: "Digit6" },
|
||||
6: { key: "Digit6", shift: true },
|
||||
"^": { key: "Digit6", altRight: true },
|
||||
"è": { key: "Digit7" },
|
||||
7: { key: "Digit7", shift: true },
|
||||
"!": { key: "Digit8" },
|
||||
8: { key: "Digit8", shift: 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: "Equal", deadKey: true },
|
||||
"_": { key: "Equal", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"$": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", altRight: 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: "Slash", deadKey: 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,15 @@
|
|||
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: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", 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: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"§": { 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>;
|
|
@ -43,7 +43,7 @@ export const keys = {
|
|||
F13: 0x68,
|
||||
Home: 0x4a,
|
||||
Insert: 0x49,
|
||||
IntlBackslash: 0x31,
|
||||
IntlBackslash: 0x64,
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
|
@ -104,116 +104,6 @@ export const keys = {
|
|||
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 },
|
||||
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, { key: string | number; shift: boolean }>;
|
||||
|
||||
export const modifiers = {
|
||||
ControlLeft: 0x01,
|
||||
ControlRight: 0x10,
|
||||
|
|
|
@ -32,7 +32,8 @@ 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";
|
||||
|
@ -41,6 +42,7 @@ import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
|
|||
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
|
||||
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
|
||||
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
|
||||
import SettingsGeneralRebootRoute from "./routes/devices.$id.settings.general.reboot";
|
||||
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
|
||||
import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
|
||||
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
|
||||
|
@ -139,6 +141,10 @@ if (isOnDevice) {
|
|||
index: true,
|
||||
element: <SettingsGeneralIndexRoute.default />,
|
||||
},
|
||||
{
|
||||
path: "reboot",
|
||||
element: <SettingsGeneralRebootRoute />,
|
||||
},
|
||||
{
|
||||
path: "update",
|
||||
element: <SettingsGeneralUpdateRoute />,
|
||||
|
@ -147,7 +153,11 @@ if (isOnDevice) {
|
|||
},
|
||||
{
|
||||
path: "mouse",
|
||||
element: <SettingsKeyboardMouseRoute />,
|
||||
element: <SettingsMouseRoute />,
|
||||
},
|
||||
{
|
||||
path: "keyboard",
|
||||
element: <SettingsKeyboardRoute />,
|
||||
},
|
||||
{
|
||||
path: "advanced",
|
||||
|
@ -276,7 +286,11 @@ if (isOnDevice) {
|
|||
},
|
||||
{
|
||||
path: "mouse",
|
||||
element: <SettingsKeyboardMouseRoute />,
|
||||
element: <SettingsMouseRoute />,
|
||||
},
|
||||
{
|
||||
path: "keyboard",
|
||||
element: <SettingsKeyboardRoute />,
|
||||
},
|
||||
{
|
||||
path: "advanced",
|
||||
|
@ -286,6 +300,10 @@ if (isOnDevice) {
|
|||
path: "hardware",
|
||||
element: <SettingsHardwareRoute />,
|
||||
},
|
||||
{
|
||||
path: "network",
|
||||
element: <SettingsNetworkRoute />,
|
||||
},
|
||||
{
|
||||
path: "access",
|
||||
children: [
|
||||
|
@ -341,10 +359,11 @@ if (isOnDevice) {
|
|||
loader: DeviceIdRename.loader,
|
||||
action: DeviceIdRename.action,
|
||||
},
|
||||
{
|
||||
path: "devices",
|
||||
{
|
||||
path: "devices",
|
||||
element: <DevicesRoute />,
|
||||
loader: DevicesRoute.loader },
|
||||
loader: DevicesRoute.loader
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
LuCheck,
|
||||
LuUpload,
|
||||
} from "react-icons/lu";
|
||||
import { PlusCircleIcon , ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
@ -38,7 +38,6 @@ import {
|
|||
useRTCStore,
|
||||
} from "../hooks/stores";
|
||||
|
||||
|
||||
export default function MountRoute() {
|
||||
const navigate = useNavigate();
|
||||
{
|
||||
|
@ -283,8 +282,8 @@ function ModeSelectionView({
|
|||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="animate-fadeIn space-y-0">
|
||||
<h2 className="text-lg font-bold leading-tight dark:text-white">
|
||||
<div className="animate-fadeIn space-y-0 opacity-0">
|
||||
<h2 className="text-lg leading-tight font-bold dark:text-white">
|
||||
Virtual Media Source
|
||||
</h2>
|
||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
|
@ -320,7 +319,7 @@ function ModeSelectionView({
|
|||
].map(({ label, description, value: mode, icon: Icon, tag, disabled }, index) => (
|
||||
<div
|
||||
key={label}
|
||||
className={cx("animate-fadeIn")}
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: `${25 * (index * 5)}ms`,
|
||||
|
@ -337,7 +336,7 @@ function ModeSelectionView({
|
|||
)}
|
||||
>
|
||||
<div
|
||||
className="relative z-50 flex select-none flex-col items-start p-4"
|
||||
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||
onClick={() =>
|
||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
||||
}
|
||||
|
@ -365,7 +364,7 @@ function ModeSelectionView({
|
|||
value={mode}
|
||||
disabled={disabled}
|
||||
checked={selectedMode === mode}
|
||||
className="absolute right-4 top-4 h-4 w-4 text-blue-700"
|
||||
className="absolute top-4 right-4 form-radio h-4 w-4 rounded-full text-blue-700"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -373,7 +372,7 @@ function ModeSelectionView({
|
|||
))}
|
||||
</div>
|
||||
<div
|
||||
className="flex animate-fadeIn justify-end"
|
||||
className="flex animate-fadeIn justify-end opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
@ -437,19 +436,19 @@ function BrowserFileView({
|
|||
className="block cursor-pointer select-none"
|
||||
>
|
||||
<div
|
||||
className="group animate-fadeIn"
|
||||
className="group animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
>
|
||||
<Card className="outline-dashed transition-all duration-300 hover:bg-blue-50/50">
|
||||
<Card className="transition-all duration-300 outline-dashed">
|
||||
<div className="w-full px-4 py-12">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<LuHardDrive className="mx-auto h-6 w-6 text-blue-700" />
|
||||
<h3 className="text-sm font-semibold leading-none">
|
||||
<h3 className="text-sm leading-none font-semibold">
|
||||
{formatters.truncateMiddle(selectedFile.name, 40)}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700">
|
||||
|
@ -460,7 +459,7 @@ function BrowserFileView({
|
|||
) : (
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700" />
|
||||
<h3 className="text-sm font-semibold leading-none">
|
||||
<h3 className="text-sm leading-none font-semibold">
|
||||
Click to select a file
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700">
|
||||
|
@ -483,7 +482,7 @@ function BrowserFileView({
|
|||
</div>
|
||||
|
||||
<div
|
||||
className="flex w-full animate-fadeIn items-end justify-between"
|
||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -578,7 +577,7 @@ function UrlView({
|
|||
/>
|
||||
|
||||
<div
|
||||
className="animate-fadeIn"
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
|
@ -593,7 +592,7 @@ function UrlView({
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-full animate-fadeIn items-end justify-between"
|
||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -619,7 +618,7 @@ function UrlView({
|
|||
|
||||
<hr className="border-slate-800/30 dark:border-slate-300/20" />
|
||||
<div
|
||||
className="animate-fadeIn"
|
||||
className="animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
|
@ -628,13 +627,13 @@ function UrlView({
|
|||
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
|
||||
Popular images
|
||||
</h2>
|
||||
<Card className="divide-y-slate-800/30 w-full divide-y dark:divide-slate-300/20">
|
||||
<Card className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||
{popularImages.map((image, index) => (
|
||||
<div key={index} className="flex items-center justify-between gap-x-4 p-3.5">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<img src={image.icon} alt={`${image.name} Icon`} className="w-6" />
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h3 className="text-sm font-semibold leading-none dark:text-white">
|
||||
<h3 className="text-sm leading-none font-semibold dark:text-white">
|
||||
{formatters.truncateMiddle(image.name, 40)}
|
||||
</h3>
|
||||
{image.description && (
|
||||
|
@ -797,7 +796,7 @@ function DeviceFileView({
|
|||
description="Select an image to mount from the JetKVM storage"
|
||||
/>
|
||||
<div
|
||||
className="w-full animate-fadeIn"
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -809,7 +808,7 @@ function DeviceFileView({
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
No images available
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
|
@ -827,7 +826,7 @@ function DeviceFileView({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y-slate-800/30 w-full divide-y dark:divide-slate-300/20">
|
||||
<div className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||
{currentFiles.map((file, index) => (
|
||||
<PreUploadedImageItem
|
||||
key={index}
|
||||
|
@ -886,7 +885,7 @@ function DeviceFileView({
|
|||
|
||||
{onStorageFiles.length > 0 ? (
|
||||
<div
|
||||
className="flex animate-fadeIn items-end justify-between"
|
||||
className="flex animate-fadeIn items-end justify-between opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.15s",
|
||||
|
@ -914,7 +913,7 @@ function DeviceFileView({
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex animate-fadeIn items-end justify-end"
|
||||
className="flex animate-fadeIn items-end justify-end opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.15s",
|
||||
|
@ -927,7 +926,7 @@ function DeviceFileView({
|
|||
)}
|
||||
<hr className="border-slate-800/20 dark:border-slate-300/20" />
|
||||
<div
|
||||
className="animate-fadeIn space-y-2"
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.20s",
|
||||
|
@ -959,7 +958,7 @@ function DeviceFileView({
|
|||
|
||||
{onStorageFiles.length > 0 && (
|
||||
<div
|
||||
className="w-full animate-fadeIn"
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.25s",
|
||||
|
@ -1251,7 +1250,7 @@ function UploadFileView({
|
|||
}
|
||||
/>
|
||||
<div
|
||||
className="animate-fadeIn space-y-2"
|
||||
className="animate-fadeIn space-y-2 opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
}}
|
||||
|
@ -1267,7 +1266,7 @@ function UploadFileView({
|
|||
<div className="group">
|
||||
<Card
|
||||
className={cx("transition-all duration-300", {
|
||||
"cursor-pointer hover:bg-blue-900/50 dark:hover:bg-blue-900/50":
|
||||
"cursor-pointer hover:bg-blue-50/50 dark:hover:bg-blue-900/50":
|
||||
uploadState === "idle",
|
||||
})}
|
||||
>
|
||||
|
@ -1282,7 +1281,7 @@ function UploadFileView({
|
|||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
{incompleteFileName
|
||||
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
|
||||
: "Click to select a file"}
|
||||
|
@ -1336,7 +1335,7 @@ function UploadFileView({
|
|||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
Upload successful
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
|
@ -1365,7 +1364,7 @@ function UploadFileView({
|
|||
{/* Display upload error if present */}
|
||||
{uploadError && (
|
||||
<div
|
||||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400"
|
||||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400 opacity-0"
|
||||
style={{ animationDuration: "0.7s" }}
|
||||
>
|
||||
Error: {uploadError}
|
||||
|
@ -1373,7 +1372,7 @@ function UploadFileView({
|
|||
)}
|
||||
|
||||
<div
|
||||
className="flex w-full animate-fadeIn items-end"
|
||||
className="flex w-full animate-fadeIn items-end opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
|
@ -1422,7 +1421,7 @@ function ErrorView({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-red-600">
|
||||
<ExclamationTriangleIcon className="h-6 w-6" />
|
||||
<h2 className="text-lg font-bold leading-tight">Mount Error</h2>
|
||||
<h2 className="text-lg leading-tight font-bold">Mount Error</h2>
|
||||
</div>
|
||||
<p className="text-sm leading-snug text-slate-600">
|
||||
An error occurred while attempting to mount the media. Please try again.
|
||||
|
@ -1481,8 +1480,8 @@ function PreUploadedImageItem({
|
|||
}}
|
||||
>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="select-none space-y-0.5">
|
||||
<div className="text-sm font-semibold leading-none dark:text-white">
|
||||
<div className="space-y-0.5 select-none">
|
||||
<div className="text-sm leading-none font-semibold dark:text-white">
|
||||
{formatters.truncateMiddle(name, 45)}
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
|
@ -1494,7 +1493,7 @@ function PreUploadedImageItem({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex select-none items-center gap-x-3">
|
||||
<div className="relative flex items-center gap-x-3 select-none">
|
||||
<div
|
||||
className={cx("opacity-0 transition-opacity duration-200", {
|
||||
"w-auto opacity-100": isHovering,
|
||||
|
@ -1518,7 +1517,7 @@ function PreUploadedImageItem({
|
|||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
name={name}
|
||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 focus:ring-blue-500 disabled:opacity-30 dark:border-slate-300/20 dark:bg-slate-800"
|
||||
className="form-radio h-3 w-3 border-slate-800/30 bg-white text-blue-700 focus:ring-blue-500 disabled:opacity-30 dark:border-slate-300/20 dark:bg-slate-800"
|
||||
onClick={e => e.stopPropagation()} // Prevent double-firing of onSelect
|
||||
/>
|
||||
) : (
|
||||
|
@ -1540,7 +1539,7 @@ function PreUploadedImageItem({
|
|||
function ViewHeader({ title, description }: { title: string; description: string }) {
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
<h2 className="text-lg font-bold leading-tight text-black dark:text-white">
|
||||
<h2 className="text-lg leading-tight font-bold text-black dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
|
@ -1558,7 +1557,7 @@ function UsbModeSelector({
|
|||
setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex select-none flex-col items-start space-y-1">
|
||||
<div className="flex flex-col items-start space-y-1 select-none">
|
||||
<label className="text-sm font-semibold text-black dark:text-white">Mount as</label>
|
||||
<div className="flex space-x-4">
|
||||
<label htmlFor="cdrom" className="flex items-center">
|
||||
|
@ -1568,7 +1567,7 @@ function UsbModeSelector({
|
|||
name="mountType"
|
||||
onChange={() => setUsbMode("CDROM")}
|
||||
checked={usbMode === "CDROM"}
|
||||
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"
|
||||
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
CD/DVD
|
||||
|
@ -1581,13 +1580,11 @@ function UsbModeSelector({
|
|||
name="mountType"
|
||||
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"
|
||||
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||
/>
|
||||
<div className="ml-2 flex flex-col gap-y-0">
|
||||
<span className="text-sm font-medium leading-none text-slate-900 opacity-50 dark:text-white">
|
||||
Disk
|
||||
</span>
|
||||
</div>
|
||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
Disk
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -409,7 +409,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
.
|
||||
</div>
|
||||
</div>
|
||||
<hr className="block w-full dark:border-slate-600" />
|
||||
<hr className="block w-full border-slate-800/20 dark:border-slate-300/20" />
|
||||
|
||||
<div>
|
||||
<LinkButton
|
||||
|
@ -469,4 +469,4 @@ export default function SettingsAccessIndexRoute() {
|
|||
);
|
||||
}
|
||||
|
||||
SettingsAccessIndexRoute.loader = loader;
|
||||
SettingsAccessIndexRoute.loader = loader;
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { GridCard } from "@components/Card";
|
||||
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import notifications from "../notifications";
|
||||
import { TextAreaWithLabel } from "../components/TextArea";
|
||||
import { isOnDevice } from "../main";
|
||||
import { Button } from "../components/Button";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { TextAreaWithLabel } from "../components/TextArea";
|
||||
import { useSettingsStore } from "../hooks/stores";
|
||||
|
||||
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import { isOnDevice } from "../main";
|
||||
import notifications from "../notifications";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
|
@ -22,6 +21,8 @@ export default function SettingsAdvancedRoute() {
|
|||
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
||||
const [devChannel, setDevChannel] = useState(false);
|
||||
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
||||
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
|
||||
const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
|
||||
|
||||
const settings = useSettingsStore();
|
||||
|
||||
|
@ -46,6 +47,11 @@ export default function SettingsAdvancedRoute() {
|
|||
if ("error" in resp) return;
|
||||
setDevChannel(resp.result as boolean);
|
||||
});
|
||||
|
||||
send("getLocalLoopbackOnly", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setLocalLoopbackOnly(resp.result as boolean);
|
||||
});
|
||||
}, [send, setDeveloperMode]);
|
||||
|
||||
const getUsbEmulationState = useCallback(() => {
|
||||
|
@ -110,17 +116,62 @@ export default function SettingsAdvancedRoute() {
|
|||
[send, setDeveloperMode],
|
||||
);
|
||||
|
||||
const handleDevChannelChange = (enabled: boolean) => {
|
||||
send("setDevChannelState", { enabled }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
const handleDevChannelChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setDevChannelState", { enabled }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setDevChannel(enabled);
|
||||
});
|
||||
},
|
||||
[send, setDevChannel],
|
||||
);
|
||||
|
||||
const applyLoopbackOnlyMode = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setLocalLoopbackOnly", { enabled }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLocalLoopbackOnly(enabled);
|
||||
if (enabled) {
|
||||
notifications.success(
|
||||
"Loopback-only mode enabled. Restart your device to apply.",
|
||||
);
|
||||
} else {
|
||||
notifications.success(
|
||||
"Loopback-only mode disabled. Restart your device to apply.",
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[send, setLocalLoopbackOnly],
|
||||
);
|
||||
|
||||
const handleLoopbackOnlyModeChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
// If trying to enable loopback-only mode, show warning first
|
||||
if (enabled) {
|
||||
setShowLoopbackWarning(true);
|
||||
} else {
|
||||
// If disabling, just proceed
|
||||
applyLoopbackOnlyMode(false);
|
||||
}
|
||||
setDevChannel(enabled);
|
||||
});
|
||||
};
|
||||
},
|
||||
[applyLoopbackOnlyMode, setShowLoopbackWarning],
|
||||
);
|
||||
|
||||
const confirmLoopbackModeEnable = useCallback(() => {
|
||||
applyLoopbackOnlyMode(true);
|
||||
setShowLoopbackWarning(false);
|
||||
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
@ -153,7 +204,7 @@ export default function SettingsAdvancedRoute() {
|
|||
|
||||
{settings.developerMode && (
|
||||
<GridCard>
|
||||
<div className="flex select-none items-start gap-x-4 p-4">
|
||||
<div className="flex items-start gap-x-4 p-4 select-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -187,6 +238,16 @@ export default function SettingsAdvancedRoute() {
|
|||
</GridCard>
|
||||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Loopback-Only Mode"
|
||||
description="Restrict web interface access to localhost only (127.0.0.1)"
|
||||
>
|
||||
<Checkbox
|
||||
checked={localLoopbackOnly}
|
||||
onChange={e => handleLoopbackOnlyModeChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{isOnDevice && settings.developerMode && (
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
|
@ -261,6 +322,30 @@ export default function SettingsAdvancedRoute() {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showLoopbackWarning}
|
||||
onClose={() => {
|
||||
setShowLoopbackWarning(false);
|
||||
}}
|
||||
title="Enable Loopback-Only Mode?"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
WARNING: This will restrict web interface access to localhost (127.0.0.1)
|
||||
only.
|
||||
</p>
|
||||
<p>Before enabling this feature, make sure you have either:</p>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>SSH access configured and tested</li>
|
||||
<li>Cloud access enabled and working</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
variant="warning"
|
||||
confirmText="I Understand, Enable Anyway"
|
||||
onConfirm={confirmLoopbackModeEnable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -92,6 +92,21 @@ export default function SettingsGeneralRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title="Reboot Device"
|
||||
description="Power cycle the JetKVM"
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reboot Device"
|
||||
onClick={() => navigateTo("./reboot")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
// This is where we send the RPC to the golang binary
|
||||
send("reboot", {force: true});
|
||||
}, [send]);
|
||||
|
||||
{
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
}
|
||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onConfirmUpdate: () => void;
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
<ConfirmationBox
|
||||
onYes={onConfirmUpdate}
|
||||
onNo={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmationBox({
|
||||
onYes,
|
||||
onNo,
|
||||
}: {
|
||||
onYes: () => void;
|
||||
onNo: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Reboot JetKVM
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Do you want to proceed with rebooting the system?
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { layouts } from "@/keyboardLayouts";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
export default function SettingsKeyboardRoute() {
|
||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
||||
const setKeyboardLayout = useSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
const setKeyboardLedSync = useSettingsStore(
|
||||
state => state.setKeyboardLedSync,
|
||||
);
|
||||
const setShowPressedKeys = useSettingsStore(
|
||||
state => state.setShowPressedKeys,
|
||||
);
|
||||
|
||||
// this ensures we always get the original en_US if it hasn't been set yet
|
||||
const safeKeyboardLayout = useMemo(() => {
|
||||
if (keyboardLayout && keyboardLayout.length > 0)
|
||||
return keyboardLayout;
|
||||
return "en_US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||
const ledSyncOptions = [
|
||||
{ value: "auto", label: "Automatic" },
|
||||
{ value: "browser", label: "Browser Only" },
|
||||
{ value: "host", label: "Host Only" },
|
||||
];
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
useEffect(() => {
|
||||
send("getKeyboardLayout", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
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 settings for your device"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{ /* 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={safeKeyboardLayout}
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{ /* 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="LED state synchronization"
|
||||
description="Synchronize the LED state of the keyboard with the target device"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
fullWidth
|
||||
value={keyboardLedSync}
|
||||
onChange={e => setKeyboardLedSync(e.target.value as KeyboardLedSync)}
|
||||
options={ledSyncOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Show Pressed Keys"
|
||||
description="Display currently pressed keys in the status bar"
|
||||
>
|
||||
<Checkbox
|
||||
checked={showPressedKeys}
|
||||
onChange={e => setShowPressedKeys(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -9,13 +9,14 @@ import { useSettingsStore } from "@/hooks/stores";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
|
||||
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
||||
import { cx } from "../cva.config";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
export default function SettingsKeyboardMouseRoute() {
|
||||
export default function SettingsMouseRoute() {
|
||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
||||
|
||||
|
@ -26,6 +27,19 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
|
||||
const [jiggler, setJiggler] = useState(false);
|
||||
|
||||
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
|
||||
const setScrollThrottling = useSettingsStore(
|
||||
state => state.setScrollThrottling,
|
||||
);
|
||||
|
||||
const scrollThrottlingOptions = [
|
||||
{ value: "0", label: "Off" },
|
||||
{ value: "10", label: "Low" },
|
||||
{ value: "25", label: "Medium" },
|
||||
{ value: "50", label: "High" },
|
||||
{ value: "100", label: "Very High" },
|
||||
];
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -65,6 +79,21 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Scroll Throttling"
|
||||
description="Reduce the frequency of scroll events"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
className="max-w-[292px]"
|
||||
value={scrollThrottling}
|
||||
fullWidth
|
||||
onChange={e => setScrollThrottling(parseInt(e.target.value))}
|
||||
options={scrollThrottlingOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Jiggler"
|
||||
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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 { LuEthernetPort } from "react-icons/lu";
|
||||
|
||||
import {
|
||||
IPv4Mode,
|
||||
|
@ -16,13 +16,18 @@ import {
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import InputField from "@components/InputField";
|
||||
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||
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 Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
||||
import EmptyCard from "../components/EmptyCard";
|
||||
import AutoHeight from "../components/AutoHeight";
|
||||
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
@ -56,15 +61,11 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
|||
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm font-medium">{remaining && <> {remaining}</>}</span>
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
{" "}
|
||||
({dayjs(lifetime).format("YYYY-MM-DD HH:mm")})
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -114,6 +115,14 @@ export default function SettingsNetworkRoute() {
|
|||
});
|
||||
}, [send]);
|
||||
|
||||
const getNetworkState = useCallback(() => {
|
||||
send("getNetworkState", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
console.log(resp.result);
|
||||
setNetworkState(resp.result as NetworkState);
|
||||
});
|
||||
}, [send, setNetworkState]);
|
||||
|
||||
const setNetworkSettingsRemote = useCallback(
|
||||
(settings: NetworkSettings) => {
|
||||
setNetworkSettingsLoaded(false);
|
||||
|
@ -129,21 +138,14 @@ export default function SettingsNetworkRoute() {
|
|||
// 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);
|
||||
getNetworkState();
|
||||
setNetworkSettingsLoaded(true);
|
||||
notifications.success("Network settings saved");
|
||||
});
|
||||
},
|
||||
[send],
|
||||
[getNetworkState, send],
|
||||
);
|
||||
|
||||
const getNetworkState = useCallback(() => {
|
||||
send("getNetworkState", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
console.log(resp.result);
|
||||
setNetworkState(resp.result as NetworkState);
|
||||
});
|
||||
}, [send, setNetworkState]);
|
||||
|
||||
const handleRenewLease = useCallback(() => {
|
||||
send("renewDHCPLease", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
|
@ -171,10 +173,6 @@ export default function SettingsNetworkRoute() {
|
|||
setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode });
|
||||
};
|
||||
|
||||
// const handleLldpTxTlvsChange = (value: string[]) => {
|
||||
// setNetworkSettings({ ...networkSettings, lldp_tx_tlvs: value });
|
||||
// };
|
||||
|
||||
const handleMdnsModeChange = (value: mDNSMode | string) => {
|
||||
setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode });
|
||||
};
|
||||
|
@ -230,7 +228,6 @@ export default function SettingsNetworkRoute() {
|
|||
size="SM"
|
||||
value={networkState?.mac_address}
|
||||
error={""}
|
||||
disabled={true}
|
||||
readOnly={true}
|
||||
className="dark:!text-opacity-60"
|
||||
/>
|
||||
|
@ -258,7 +255,7 @@ export default function SettingsNetworkRoute() {
|
|||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<SettingsItem
|
||||
title="Domain"
|
||||
description="Network domain suffix for the device"
|
||||
|
@ -277,19 +274,17 @@ export default function SettingsNetworkRoute() {
|
|||
</div>
|
||||
</SettingsItem>
|
||||
{selectedDomainOption === "custom" && (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<InputField
|
||||
<div className="mt-2 w-1/3 border-l border-slate-800/10 pl-4 dark:border-slate-300/20">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
type="text"
|
||||
label="Custom Domain"
|
||||
placeholder="home"
|
||||
value={customDomain}
|
||||
onChange={e => setCustomDomain(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Save Domain"
|
||||
onClick={() => handleCustomDomainChange(customDomain)}
|
||||
onChange={e => {
|
||||
setCustomDomain(e.target.value);
|
||||
handleCustomDomainChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -359,209 +354,35 @@ export default function SettingsNetworkRoute() {
|
|||
])}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
<AutoHeight>
|
||||
{!networkSettingsLoaded && !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 Information
|
||||
</h3>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||
</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>
|
||||
)}
|
||||
</GridCard>
|
||||
) : networkState?.dhcp_lease && networkState.dhcp_lease.ip ? (
|
||||
<DhcpLeaseCard
|
||||
networkState={networkState}
|
||||
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
||||
/>
|
||||
) : (
|
||||
<EmptyCard
|
||||
IconElm={LuEthernetPort}
|
||||
headline="DHCP Information"
|
||||
description="No DHCP lease information available"
|
||||
/>
|
||||
)}
|
||||
</AutoHeight>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||
|
@ -579,93 +400,33 @@ export default function SettingsNetworkRoute() {
|
|||
])}
|
||||
/>
|
||||
</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="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>
|
||||
)}
|
||||
<AutoHeight>
|
||||
{!networkSettingsLoaded &&
|
||||
!(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
|
||||
<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="animate-pulse space-y-3">
|
||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</GridCard>
|
||||
) : networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0 ? (
|
||||
<Ipv6NetworkCard networkState={networkState} />
|
||||
) : (
|
||||
<EmptyCard
|
||||
IconElm={LuEthernetPort}
|
||||
headline="IPv6 Information"
|
||||
description="No IPv6 addresses configured"
|
||||
/>
|
||||
)}
|
||||
</AutoHeight>
|
||||
</div>
|
||||
<div className="hidden space-y-4">
|
||||
<SettingsItem
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
import {
|
||||
LuSettings,
|
||||
LuMouse,
|
||||
LuKeyboard,
|
||||
LuVideo,
|
||||
LuCpu,
|
||||
|
@ -20,6 +21,7 @@ import LoadingSpinner from "@/components/LoadingSpinner";
|
|||
import { useUiStore } from "@/hooks/stores";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { cx } from "../cva.config";
|
||||
|
||||
|
||||
|
@ -149,11 +151,25 @@ 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 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.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>
|
||||
<FeatureFlag minAppVersion="0.4.0" name="Paste text">
|
||||
<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>
|
||||
</FeatureFlag>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
to="video"
|
||||
|
@ -253,22 +269,17 @@ export default function SettingsRoute() {
|
|||
);
|
||||
}
|
||||
|
||||
export function SettingsItem({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
loading,
|
||||
badge,
|
||||
}: {
|
||||
title: string;
|
||||
description: string | React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
name?: string;
|
||||
loading?: boolean;
|
||||
badge?: string;
|
||||
}) {
|
||||
interface SettingsItemProps {
|
||||
readonly title: string;
|
||||
readonly description: string | React.ReactNode;
|
||||
readonly badge?: string;
|
||||
readonly className?: string;
|
||||
readonly loading?: boolean;
|
||||
readonly children?: React.ReactNode;
|
||||
}
|
||||
export function SettingsItem(props: SettingsItemProps) {
|
||||
const { title, description, badge, children, className, loading } = props;
|
||||
|
||||
return (
|
||||
<label
|
||||
className={cx(
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Button } from "@/components/Button";
|
|||
import { TextAreaWithLabel } from "@/components/TextArea";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
@ -45,6 +46,14 @@ export default function SettingsVideoRoute() {
|
|||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||
const [edid, setEdid] = useState<string | null>(null);
|
||||
|
||||
// Video enhancement settings from store
|
||||
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
||||
const setVideoSaturation = useSettingsStore(state => state.setVideoSaturation);
|
||||
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
||||
const setVideoBrightness = useSettingsStore(state => state.setVideoBrightness);
|
||||
const videoContrast = useSettingsStore(state => state.videoContrast);
|
||||
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
||||
|
||||
useEffect(() => {
|
||||
send("getStreamQualityFactor", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
|
@ -126,6 +135,73 @@ export default function SettingsVideoRoute() {
|
|||
onChange={e => handleStreamQualityChange(e.target.value)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{/* Video Enhancement Settings */}
|
||||
<SettingsItem
|
||||
title="Video Enhancement"
|
||||
description="Adjust color settings to make the video output more vibrant and colorful"
|
||||
/>
|
||||
|
||||
<div className="space-y-4 pl-4">
|
||||
<SettingsItem
|
||||
title="Saturation"
|
||||
description={`Color saturation (${videoSaturation.toFixed(1)}x)`}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="2.0"
|
||||
step="0.1"
|
||||
value={videoSaturation}
|
||||
onChange={e => setVideoSaturation(parseFloat(e.target.value))}
|
||||
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Brightness"
|
||||
description={`Brightness level (${videoBrightness.toFixed(1)}x)`}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
step="0.1"
|
||||
value={videoBrightness}
|
||||
onChange={e => setVideoBrightness(parseFloat(e.target.value))}
|
||||
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Contrast"
|
||||
description={`Contrast level (${videoContrast.toFixed(1)}x)`}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="2.0"
|
||||
step="0.1"
|
||||
value={videoContrast}
|
||||
onChange={e => setVideoContrast(parseFloat(e.target.value))}
|
||||
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reset to Default"
|
||||
onClick={() => {
|
||||
setVideoSaturation(1.0);
|
||||
setVideoBrightness(1.0);
|
||||
setVideoContrast(1.0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsItem
|
||||
title="EDID"
|
||||
description="Adjust the EDID settings for the display"
|
||||
|
|
|
@ -19,6 +19,7 @@ import useWebSocket from "react-use-websocket";
|
|||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
HidState,
|
||||
KeyboardLedState,
|
||||
NetworkState,
|
||||
UpdateState,
|
||||
useDeviceStore,
|
||||
|
@ -586,6 +587,11 @@ export default function KvmIdRoute() {
|
|||
const setUsbState = useHidStore(state => state.setUsbState);
|
||||
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
||||
|
||||
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
||||
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
|
||||
|
||||
const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable);
|
||||
|
||||
const [hasUpdated, setHasUpdated] = useState(false);
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
|
@ -607,6 +613,13 @@ export default function KvmIdRoute() {
|
|||
setNetworkState(resp.params as NetworkState);
|
||||
}
|
||||
|
||||
if (resp.method === "keyboardLedState") {
|
||||
const ledState = resp.params as KeyboardLedState;
|
||||
console.log("Setting keyboard led state", ledState);
|
||||
setKeyboardLedState(ledState);
|
||||
setKeyboardLedStateSyncAvailable(true);
|
||||
}
|
||||
|
||||
if (resp.method === "otaState") {
|
||||
const otaState = resp.params as UpdateState["otaState"];
|
||||
setOtaState(otaState);
|
||||
|
@ -643,6 +656,29 @@ export default function KvmIdRoute() {
|
|||
});
|
||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||
|
||||
// request keyboard led state from the device
|
||||
useEffect(() => {
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
if (keyboardLedState !== undefined) return;
|
||||
console.log("Requesting keyboard led state");
|
||||
|
||||
send("getKeyboardLedState", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
// -32601 means the method is not supported
|
||||
if (resp.error.code === -32601) {
|
||||
setKeyboardLedStateSyncAvailable(false);
|
||||
console.error("Failed to get keyboard led state, disabling sync", resp.error);
|
||||
} else {
|
||||
console.error("Failed to get keyboard led state", resp.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.log("Keyboard led state", resp.result);
|
||||
setKeyboardLedState(resp.result as KeyboardLedState);
|
||||
setKeyboardLedStateSyncAvailable(true);
|
||||
});
|
||||
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, setKeyboardLedStateSyncAvailable, keyboardLedState]);
|
||||
|
||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||
useEffect(() => {
|
||||
if (queryParams.get("updateSuccess")) {
|
||||
|
@ -679,12 +715,10 @@ export default function KvmIdRoute() {
|
|||
useEffect(() => {
|
||||
if (!peerConnection) return;
|
||||
if (!kvmTerminal) {
|
||||
// console.log('Creating data channel "terminal"');
|
||||
setKvmTerminal(peerConnection.createDataChannel("terminal"));
|
||||
}
|
||||
|
||||
if (!serialConsole) {
|
||||
// console.log('Creating data channel "serial"');
|
||||
setSerialConsole(peerConnection.createDataChannel("serial"));
|
||||
}
|
||||
}, [kvmTerminal, peerConnection, serialConsole]);
|
||||
|
@ -719,10 +753,10 @@ export default function KvmIdRoute() {
|
|||
|
||||
const ConnectionStatusElement = useMemo(() => {
|
||||
const hasConnectionFailed =
|
||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState || "");
|
||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
||||
|
||||
const isPeerConnectionLoading =
|
||||
["connecting", "new"].includes(peerConnectionState || "") ||
|
||||
["connecting", "new"].includes(peerConnectionState ?? "") ||
|
||||
peerConnection === null;
|
||||
|
||||
const isDisconnected = peerConnectionState === "disconnected";
|
||||
|
@ -790,7 +824,7 @@ export default function KvmIdRoute() {
|
|||
isLoggedIn={authMode === "password" || !!user}
|
||||
userEmail={user?.email}
|
||||
picture={user?.picture}
|
||||
kvmName={deviceName || "JetKVM Device"}
|
||||
kvmName={deviceName ?? "JetKVM Device"}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
|
@ -810,6 +844,9 @@ export default function KvmIdRoute() {
|
|||
|
||||
<div
|
||||
className="z-50"
|
||||
onClick={e => e.stopPropagation()}
|
||||
onMouseUp={e => e.stopPropagation()}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onKeyUp={e => e.stopPropagation()}
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
|
@ -833,7 +870,12 @@ export default function KvmIdRoute() {
|
|||
);
|
||||
}
|
||||
|
||||
function SidebarContainer({ sidebarView }: { sidebarView: string | null }) {
|
||||
interface SidebarContainerProps {
|
||||
readonly sidebarView: string | null;
|
||||
}
|
||||
|
||||
function SidebarContainer(props: SidebarContainerProps) {
|
||||
const { sidebarView }= props;
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
|
|
|
@ -116,7 +116,7 @@ export default function WelcomeLocalModeRoute() {
|
|||
onChange={() => {
|
||||
setSelectedMode(mode as "password" | "noPassword");
|
||||
}}
|
||||
className="absolute top-2 right-2 h-4 w-4 text-blue-600"
|
||||
className="form-radio absolute top-2 right-2 h-4 w-4 text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
|
15
usb.go
15
usb.go
|
@ -24,6 +24,17 @@ func initUsbGadget() {
|
|||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
||||
if currentSession != nil {
|
||||
writeJSONRPCEvent("keyboardLedState", state, currentSession)
|
||||
}
|
||||
})
|
||||
|
||||
// open the keyboard hid file to listen for keyboard events
|
||||
if err := gadget.OpenKeyboardHidFile(); err != nil {
|
||||
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file")
|
||||
}
|
||||
}
|
||||
|
||||
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
||||
|
@ -42,6 +53,10 @@ func rpcWheelReport(wheelY int8) error {
|
|||
return gadget.AbsMouseWheelReport(wheelY)
|
||||
}
|
||||
|
||||
func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) {
|
||||
return gadget.GetKeyboardState()
|
||||
}
|
||||
|
||||
var usbState = "unknown"
|
||||
|
||||
func rpcGetUSBState() (state string) {
|
||||
|
|
38
web.go
38
web.go
|
@ -52,8 +52,9 @@ type ChangePasswordRequest struct {
|
|||
}
|
||||
|
||||
type LocalDevice struct {
|
||||
AuthMode *string `json:"authMode"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
AuthMode *string `json:"authMode"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
LoopbackOnly bool `json:"loopbackOnly"`
|
||||
}
|
||||
|
||||
type DeviceStatus struct {
|
||||
|
@ -96,9 +97,6 @@ func setupRouter() *gin.Engine {
|
|||
// We use this to determine if the device is setup
|
||||
r.GET("/device/status", handleDeviceStatus)
|
||||
|
||||
// We use this to provide the UI with the device configuration
|
||||
r.GET("/device/ui-config.js", handleDeviceUIConfig)
|
||||
|
||||
// We use this to setup the device in the welcome page
|
||||
r.POST("/device/setup", handleSetup)
|
||||
|
||||
|
@ -532,7 +530,15 @@ func basicAuthProtectedMiddleware(requireDeveloperMode bool) gin.HandlerFunc {
|
|||
|
||||
func RunWebServer() {
|
||||
r := setupRouter()
|
||||
err := r.Run(":80")
|
||||
|
||||
// Determine the binding address based on the config
|
||||
bindAddress := ":80" // Default to all interfaces
|
||||
if config.LocalLoopbackOnly {
|
||||
bindAddress = "localhost:80" // Loopback only (both IPv4 and IPv6)
|
||||
}
|
||||
|
||||
logger.Info().Str("bindAddress", bindAddress).Bool("loopbackOnly", config.LocalLoopbackOnly).Msg("Starting web server")
|
||||
err := r.Run(bindAddress)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -540,8 +546,9 @@ func RunWebServer() {
|
|||
|
||||
func handleDevice(c *gin.Context) {
|
||||
response := LocalDevice{
|
||||
AuthMode: &config.LocalAuthMode,
|
||||
DeviceID: GetDeviceID(),
|
||||
AuthMode: &config.LocalAuthMode,
|
||||
DeviceID: GetDeviceID(),
|
||||
LoopbackOnly: config.LocalLoopbackOnly,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
|
@ -684,21 +691,6 @@ func handleCloudState(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func handleDeviceUIConfig(c *gin.Context) {
|
||||
config, _ := json.Marshal(gin.H{
|
||||
"CLOUD_API": config.CloudURL,
|
||||
"DEVICE_VERSION": builtAppVersion,
|
||||
})
|
||||
if config == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
|
||||
return
|
||||
}
|
||||
|
||||
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
|
||||
|
||||
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
|
||||
}
|
||||
|
||||
func handleSetup(c *gin.Context) {
|
||||
// Check if the device is already set up
|
||||
if config.LocalAuthMode != "" || config.HashedPassword != "" {
|
||||
|
|
|
@ -108,6 +108,9 @@ func setTLSState(s TLSState) error {
|
|||
isChanged = true
|
||||
}
|
||||
// parse pem to cert and key
|
||||
if certStore == nil {
|
||||
initCertStore()
|
||||
}
|
||||
err, _ := certStore.ValidateAndSaveCertificate(webSecureCustomCertificateName, s.Certificate, s.PrivateKey, true)
|
||||
// warn doesn't matter as ... we don't know the hostname yet
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in New Issue