Compare commits
10 Commits
fb2ae8481c
...
a4e036703a
| Author | SHA1 | Date |
|---|---|---|
|
|
a4e036703a | |
|
|
5f3dd89d55 | |
|
|
1dda6184da | |
|
|
825d0311d6 | |
|
|
f3fe78af5d | |
|
|
d0b3781aaa | |
|
|
c68e15bf89 | |
|
|
94521ef6db | |
|
|
66cccfe9e1 | |
|
|
a42384fed6 |
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
|
if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.2.2
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1
|
uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -22,25 +22,12 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
return 0, errors.New("image not mounted")
|
return 0, errors.New("image not mounted")
|
||||||
}
|
}
|
||||||
source := currentVirtualMediaState.Source
|
source := currentVirtualMediaState.Source
|
||||||
mountedImageSize := currentVirtualMediaState.Size
|
|
||||||
virtualMediaStateMutex.RUnlock()
|
virtualMediaStateMutex.RUnlock()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
_, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
readLen := int64(len(p))
|
|
||||||
if off+readLen > mountedImageSize {
|
|
||||||
readLen = mountedImageSize - off
|
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
switch source {
|
switch source {
|
||||||
case WebRTC:
|
|
||||||
data, err = webRTCDiskReader.Read(ctx, off, readLen)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
n = copy(p, data)
|
|
||||||
return n, nil
|
|
||||||
case HTTP:
|
case HTTP:
|
||||||
return httpRangeReader.ReadAt(p, off)
|
return httpRangeReader.ReadAt(p, off)
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
114
fuse.go
|
|
@ -1,114 +0,0 @@
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WebRTCStreamFile struct {
|
|
||||||
fs.Inode
|
|
||||||
mu sync.Mutex
|
|
||||||
Attr fuse.Attr
|
|
||||||
size uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
|
||||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
|
||||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
|
||||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
|
||||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
|
||||||
|
|
||||||
func (f *WebRTCStreamFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
|
||||||
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *WebRTCStreamFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
|
|
||||||
return 0, syscall.EROFS
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = (fs.NodeGetattrer)((*WebRTCStreamFile)(nil))
|
|
||||||
|
|
||||||
func (f *WebRTCStreamFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
out.Attr = f.Attr
|
|
||||||
out.Size = f.size
|
|
||||||
return fs.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *WebRTCStreamFile) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
out.Attr = f.Attr
|
|
||||||
return fs.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *WebRTCStreamFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
|
|
||||||
return fs.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiskReadRequest struct {
|
|
||||||
Start uint64 `json:"start"`
|
|
||||||
End uint64 `json:"end"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var diskReadChan = make(chan []byte, 1)
|
|
||||||
|
|
||||||
func (f *WebRTCStreamFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
|
|
||||||
buf, err := webRTCDiskReader.Read(ctx, off, int64(len(dest)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, syscall.EIO
|
|
||||||
}
|
|
||||||
return fuse.ReadResultData(buf), fs.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *WebRTCStreamFile) SetSize(size uint64) {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
f.size = size
|
|
||||||
}
|
|
||||||
|
|
||||||
type FuseRoot struct {
|
|
||||||
fs.Inode
|
|
||||||
}
|
|
||||||
|
|
||||||
var webRTCStreamFile = &WebRTCStreamFile{}
|
|
||||||
|
|
||||||
func (r *FuseRoot) OnAdd(ctx context.Context) {
|
|
||||||
ch := r.NewPersistentInode(ctx, webRTCStreamFile, fs.StableAttr{Ino: 2})
|
|
||||||
r.AddChild("disk", ch, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FuseRoot) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
|
||||||
out.Mode = 0755
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = (fs.NodeGetattrer)((*FuseRoot)(nil))
|
|
||||||
var _ = (fs.NodeOnAdder)((*FuseRoot)(nil))
|
|
||||||
|
|
||||||
const fuseMountPoint = "/mnt/webrtc"
|
|
||||||
|
|
||||||
var fuseServer *fuse.Server
|
|
||||||
|
|
||||||
func RunFuseServer() {
|
|
||||||
opts := &fs.Options{}
|
|
||||||
opts.DirectMountStrict = true
|
|
||||||
_ = os.Mkdir(fuseMountPoint, 0755)
|
|
||||||
var err error
|
|
||||||
fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to mount fuse")
|
|
||||||
}
|
|
||||||
fuseServer.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebRTCImage struct {
|
|
||||||
Size uint64 `json:"size"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
}
|
|
||||||
39
go.mod
|
|
@ -6,32 +6,31 @@ require (
|
||||||
github.com/Masterminds/semver/v3 v3.4.0
|
github.com/Masterminds/semver/v3 v3.4.0
|
||||||
github.com/beevik/ntp v1.4.3
|
github.com/beevik/ntp v1.4.3
|
||||||
github.com/coder/websocket v1.8.13
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
github.com/coreos/go-oidc/v3 v3.15.0
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/gin-contrib/logger v1.2.6
|
github.com/gin-contrib/logger v1.2.6
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3
|
github.com/go-co-op/gocron/v2 v2.16.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/guregu/null/v6 v6.0.0
|
github.com/guregu/null/v6 v6.0.0
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
||||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
|
||||||
github.com/pion/logging v0.2.4
|
github.com/pion/logging v0.2.4
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.1.3
|
github.com/pion/webrtc/v4 v4.1.4
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.23.0
|
||||||
github.com/prometheus/common v0.65.0
|
github.com/prometheus/common v0.66.0
|
||||||
github.com/prometheus/procfs v0.16.1
|
github.com/prometheus/procfs v0.17.0
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
go.bug.st/serial v1.6.4
|
go.bug.st/serial v1.6.4
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/net v0.41.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sys v0.34.0
|
golang.org/x/sys v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||||
|
|
@ -51,6 +50,7 @@ require (
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
|
@ -63,18 +63,18 @@ require (
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pilebones/go-udev v0.9.1 // indirect
|
github.com/pilebones/go-udev v0.9.1 // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
github.com/pion/dtls/v3 v3.0.7 // indirect
|
||||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||||
github.com/pion/interceptor v0.1.40 // indirect
|
github.com/pion/interceptor v0.1.40 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.15 // indirect
|
github.com/pion/rtcp v1.2.15 // indirect
|
||||||
github.com/pion/rtp v1.8.20 // indirect
|
github.com/pion/rtp v1.8.22 // indirect
|
||||||
github.com/pion/sctp v1.8.39 // indirect
|
github.com/pion/sctp v1.8.39 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.14 // indirect
|
github.com/pion/sdp/v3 v3.0.16 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.6 // indirect
|
github.com/pion/srtp/v3 v3.0.7 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
|
@ -85,7 +85,8 @@ require (
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/arch v0.18.0 // indirect
|
golang.org/x/arch v0.18.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/text v0.27.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
80
go.sum
|
|
@ -18,8 +18,8 @@ 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/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 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
|
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
|
||||||
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||||
|
|
@ -38,8 +38,8 @@ 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-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 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
|
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
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-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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
|
@ -58,12 +58,12 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||||
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
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/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoNJH4uUmsQ86+0/QqpwEns0NyNLwKv0=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/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/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
|
@ -92,8 +92,6 @@ 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|
@ -107,8 +105,8 @@ github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3
|
||||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
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 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
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.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
||||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
||||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
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/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 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
|
|
@ -121,33 +119,33 @@ 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/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
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.22 h1:8NCVDDF+uSJmMUkjLJVnIr/HX7gPesyMV1xFt5xozXc=
|
||||||
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
github.com/pion/rtp v1.8.22/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
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/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.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||||
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
|
github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs=
|
||||||
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
|
github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0=
|
||||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
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/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 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||||
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
|
github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M=
|
||||||
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
|
github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
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/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.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
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/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
|
@ -167,8 +165,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||||
github.com/stretchr/testify v1.7.1/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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
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/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
|
@ -185,10 +183,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
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/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
@ -196,15 +194,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,10 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
||||||
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
|
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
|
||||||
} else if addr.IP.To16() != nil {
|
} else if addr.IP.To16() != nil {
|
||||||
|
if s.config.IPv6Mode.String == "disabled" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
||||||
// check if it's a link local address
|
// check if it's a link local address
|
||||||
if addr.IP.IsLinkLocalUnicast() {
|
if addr.IP.IsLinkLocalUnicast() {
|
||||||
|
|
@ -287,35 +291,37 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
}
|
}
|
||||||
s.ipv4Addresses = ipv4AddressesString
|
s.ipv4Addresses = ipv4AddressesString
|
||||||
|
|
||||||
if ipv6LinkLocal != nil {
|
if s.config.IPv6Mode.String != "disabled" {
|
||||||
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
if ipv6LinkLocal != nil {
|
||||||
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
||||||
if s.ipv6LinkLocal != nil {
|
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
||||||
scopedLogger.Info().
|
if s.ipv6LinkLocal != nil {
|
||||||
Str("old_ipv6", s.ipv6LinkLocal.String()).
|
scopedLogger.Info().
|
||||||
Msg("IPv6 link local address changed")
|
Str("old_ipv6", s.ipv6LinkLocal.String()).
|
||||||
} else {
|
Msg("IPv6 link local address changed")
|
||||||
scopedLogger.Info().Msg("IPv6 link local address found")
|
} else {
|
||||||
|
scopedLogger.Info().Msg("IPv6 link local address found")
|
||||||
|
}
|
||||||
|
s.ipv6LinkLocal = ipv6LinkLocal
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
s.ipv6LinkLocal = ipv6LinkLocal
|
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
}
|
s.ipv6Addresses = ipv6Addresses
|
||||||
s.ipv6Addresses = ipv6Addresses
|
|
||||||
|
|
||||||
if len(ipv6Addresses) > 0 {
|
if len(ipv6Addresses) > 0 {
|
||||||
// compare the addresses to see if there's a change
|
// compare the addresses to see if there's a change
|
||||||
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
|
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
|
||||||
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
|
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
|
||||||
if s.ipv6Addr != nil {
|
if s.ipv6Addr != nil {
|
||||||
scopedLogger.Info().
|
scopedLogger.Info().
|
||||||
Str("old_ipv6", s.ipv6Addr.String()).
|
Str("old_ipv6", s.ipv6Addr.String()).
|
||||||
Msg("IPv6 address changed")
|
Msg("IPv6 address changed")
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("IPv6 address found")
|
scopedLogger.Info().Msg("IPv6 address found")
|
||||||
|
}
|
||||||
|
s.ipv6Addr = &ipv6Addresses[0].Address
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
s.ipv6Addr = &ipv6Addresses[0].Address
|
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
|
||||||
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
||||||
ipv6Addresses := make([]RpcIPv6Address, 0)
|
ipv6Addresses := make([]RpcIPv6Address, 0)
|
||||||
|
|
||||||
if s.ipv6Addresses != nil {
|
if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" {
|
||||||
for _, addr := range s.ipv6Addresses {
|
for _, addr := range s.ipv6Addresses {
|
||||||
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
||||||
Address: addr.Prefix.String(),
|
Address: addr.Prefix.String(),
|
||||||
|
|
|
||||||
|
|
@ -1103,7 +1103,6 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getVirtualMediaState": {Func: rpcGetVirtualMediaState},
|
"getVirtualMediaState": {Func: rpcGetVirtualMediaState},
|
||||||
"getStorageSpace": {Func: rpcGetStorageSpace},
|
"getStorageSpace": {Func: rpcGetStorageSpace},
|
||||||
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
||||||
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
|
|
||||||
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
||||||
"listStorageFiles": {Func: rpcListStorageFiles},
|
"listStorageFiles": {Func: rpcListStorageFiles},
|
||||||
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
|
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemoteImageReader interface {
|
|
||||||
Read(ctx context.Context, offset int64, size int64) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebRTCDiskReader struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
var webRTCDiskReader WebRTCDiskReader
|
|
||||||
|
|
||||||
func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ([]byte, error) {
|
|
||||||
virtualMediaStateMutex.RLock()
|
|
||||||
if currentVirtualMediaState == nil {
|
|
||||||
virtualMediaStateMutex.RUnlock()
|
|
||||||
return nil, errors.New("image not mounted")
|
|
||||||
}
|
|
||||||
if currentVirtualMediaState.Source != WebRTC {
|
|
||||||
virtualMediaStateMutex.RUnlock()
|
|
||||||
return nil, errors.New("image not mounted from webrtc")
|
|
||||||
}
|
|
||||||
mountedImageSize := currentVirtualMediaState.Size
|
|
||||||
virtualMediaStateMutex.RUnlock()
|
|
||||||
end := min(offset+size, mountedImageSize)
|
|
||||||
req := DiskReadRequest{
|
|
||||||
Start: uint64(offset),
|
|
||||||
End: uint64(end),
|
|
||||||
}
|
|
||||||
jsonBytes, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentSession == nil || currentSession.DiskChannel == nil {
|
|
||||||
return nil, errors.New("not active session")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug().Str("request", string(jsonBytes)).Msg("reading from webrtc")
|
|
||||||
err = currentSession.DiskChannel.SendText(string(jsonBytes))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var buf []byte
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case data := <-diskReadChan:
|
|
||||||
buf = data[16:]
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, context.Canceled
|
|
||||||
}
|
|
||||||
if len(buf) >= int(end-offset) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<!-- These are the fonts used in the app -->
|
<!-- These are the fonts used in the app -->
|
||||||
<link
|
<link
|
||||||
|
|
@ -27,7 +27,14 @@
|
||||||
/>
|
/>
|
||||||
<title>JetKVM</title>
|
<title>JetKVM</title>
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="JetKVM" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<meta name="theme-color" content="#051946" />
|
||||||
|
<meta name="description" content="A web-based KVM console for managing remote servers." />
|
||||||
<script>
|
<script>
|
||||||
// Initial theme setup
|
// Initial theme setup
|
||||||
document.documentElement.classList.toggle(
|
document.documentElement.classList.toggle(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2025.08.25.2300",
|
"version": "2025.09.03.2100",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "22.15.0"
|
"node": "22.15.0"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.4",
|
"cva": "^1.0.0-beta.4",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^11.0.4",
|
"focus-trap-react": "^11.0.4",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
|
|
@ -41,11 +41,11 @@
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router": "^7.8.2",
|
||||||
"react-simple-keyboard": "^3.8.115",
|
"react-simple-keyboard": "^3.8.119",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^3.1.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.15",
|
||||||
|
|
@ -59,13 +59,13 @@
|
||||||
"@tailwindcss/postcss": "^4.1.12",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/validator": "^13.15.2",
|
"@types/validator": "^13.15.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.42.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^4.0.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.12",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^6.3.5",
|
"vite": "^7.1.4",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 972 B |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#1D4ED8" d="M0 6a6 6 0 0 1 6-6h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6Z"/><path fill="#fff" d="M13.885 12a1.895 1.895 0 1 1-3.79 0 1.895 1.895 0 0 1 3.79 0Z"/><path fill="#fff" fill-rule="evenodd" d="M7.59 9.363c.49.182.74.727.558 1.218A4.103 4.103 0 0 0 12 16.105a4.103 4.103 0 0 0 3.852-5.526.947.947 0 0 1 1.777-.658A5.998 5.998 0 0 1 12 18a5.998 5.998 0 0 1-5.628-8.078.947.947 0 0 1 1.218-.56ZM11.993 7.895c-.628 0-1.22.14-1.75.39a.947.947 0 1 1-.808-1.714A5.985 5.985 0 0 1 11.993 6c.913 0 1.78.204 2.557.57a.947.947 0 1 1-.808 1.715 4.09 4.09 0 0 0-1.75-.39Z" clip-rule="evenodd"/><path fill="#1D4ED8" d="M0 6a6 6 0 0 1 6-6h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6Z"/><path fill="#fff" d="M13.885 12a1.895 1.895 0 1 1-3.79 0 1.895 1.895 0 0 1 3.79 0Z"/><path fill="#fff" fill-rule="evenodd" d="M7.59 9.363c.49.182.74.727.558 1.218A4.103 4.103 0 0 0 12 16.105a4.103 4.103 0 0 0 3.852-5.526.947.947 0 0 1 1.777-.658A5.998 5.998 0 0 1 12 18a5.998 5.998 0 0 1-5.628-8.078.947.947 0 0 1 1.218-.56ZM11.993 7.895c-.628 0-1.22.14-1.75.39a.947.947 0 1 1-.808-1.714A5.985 5.985 0 0 1 11.993 6c.913 0 1.78.204 2.557.57a.947.947 0 1 1-.808 1.715 4.09 4.09 0 0 0-1.75-.39Z" clip-rule="evenodd"/></svg><style>@media (prefers-color-scheme:light){:root{filter:none}}@media (prefers-color-scheme:dark){:root{filter:none}}</style></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#1D4ED8" d="M0 6a6 6 0 0 1 6-6h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6Z"/><path fill="#fff" d="M13.885 12a1.895 1.895 0 1 1-3.79 0 1.895 1.895 0 0 1 3.79 0Z"/><path fill="#fff" fill-rule="evenodd" d="M7.59 9.363c.49.182.74.727.558 1.218A4.103 4.103 0 0 0 12 16.105a4.103 4.103 0 0 0 3.852-5.526.947.947 0 0 1 1.777-.658A5.998 5.998 0 0 1 12 18a5.998 5.998 0 0 1-5.628-8.078.947.947 0 0 1 1.218-.56ZM11.993 7.895c-.628 0-1.22.14-1.75.39a.947.947 0 1 1-.808-1.714A5.985 5.985 0 0 1 11.993 6c.913 0 1.78.204 2.557.57a.947.947 0 1 1-.808 1.715 4.09 4.09 0 0 0-1.75-.39Z" clip-rule="evenodd"/><path fill="#1D4ED8" d="M0 6a6 6 0 0 1 6-6h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6Z"/><path fill="#fff" d="M13.885 12a1.895 1.895 0 1 1-3.79 0 1.895 1.895 0 0 1 3.79 0Z"/><path fill="#fff" fill-rule="evenodd" d="M7.59 9.363c.49.182.74.727.558 1.218A4.103 4.103 0 0 0 12 16.105a4.103 4.103 0 0 0 3.852-5.526.947.947 0 0 1 1.777-.658A5.998 5.998 0 0 1 12 18a5.998 5.998 0 0 1-5.628-8.078.947.947 0 0 1 1.218-.56ZM11.993 7.895c-.628 0-1.22.14-1.75.39a.947.947 0 1 1-.808-1.714A5.985 5.985 0 0 1 11.993 6c.913 0 1.78.204 2.557.57a.947.947 0 1 1-.808 1.715 4.09 4.09 0 0 0-1.75-.39Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "JetKVM",
|
||||||
|
"short_name": "JetKVM",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#002b36",
|
||||||
|
"background_color": "#051946",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
|
@ -1,8 +0,0 @@
|
||||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2 6C2 4.89543 2.89543 4 4 4H20C21.1046 4 22 4.89543 22 6V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V6Z"
|
|
||||||
fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
||||||
d="M20 6H4V18H20V6ZM4 4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V6C22 4.89543 21.1046 4 20 4H4Z"
|
|
||||||
fill="black"/>
|
|
||||||
<path d="M4 13H20V18H4V13Z" fill="black"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 511 B |
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useNavigation, useSearchParams } from "react-router-dom";
|
import { useLocation, useNavigation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import { GoogleIcon } from "@components/Icons";
|
import { GoogleIcon } from "@components/Icons";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { JSX } from "react";
|
import React, { JSX } from "react";
|
||||||
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
import { Link, useNavigation } from "react-router";
|
||||||
|
import type { FetcherWithComponents, LinkProps } from "react-router";
|
||||||
|
|
||||||
import ExtLink from "@/components/ExtLink";
|
import ExtLink from "@/components/ExtLink";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
|
|
@ -175,7 +176,7 @@ type ButtonPropsType = Pick<
|
||||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||||
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
|
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-hidden",
|
"group outline-hidden cursor-pointer",
|
||||||
props.fullWidth ? "w-full" : "",
|
props.fullWidth ? "w-full" : "",
|
||||||
loading ? "pointer-events-none" : "",
|
loading ? "pointer-events-none" : "",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FetcherWithComponents, useNavigation } from "react-router-dom";
|
import { useNavigation } from "react-router";
|
||||||
|
import type { FetcherWithComponents } from "react-router";
|
||||||
|
|
||||||
export default function Fieldset({
|
export default function Fieldset({
|
||||||
children,
|
children,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export default function Ipv6NetworkCard({
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||||
{networkState?.dhcp_lease?.ip && (
|
{networkState?.ipv6_link_local && (
|
||||||
<div className="flex flex-col justify-between">
|
<div className="flex flex-col justify-between">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Link-local
|
Link-local
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { MdConnectWithoutContact } from "react-icons/md";
|
import { MdConnectWithoutContact } from "react-icons/md";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import { LuEllipsisVertical } from "react-icons/lu";
|
import { LuEllipsisVertical } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Container from "@/components/Container";
|
import Container from "@/components/Container";
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,31 @@ import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Keyboard from "react-simple-keyboard";
|
import Keyboard from "react-simple-keyboard";
|
||||||
|
import { LuKeyboard } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
import { Button } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
|
|
||||||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
|
||||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
import { useHidStore, useUiStore } from "@/hooks/stores";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings";
|
import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AttachIcon = ({ className }: { className?: string }) => {
|
|
||||||
return <img src={AttachIconRaw} alt="Attach Icon" className={className} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
function KeyboardWrapper() {
|
function KeyboardWrapper() {
|
||||||
const keyboardRef = useRef<HTMLDivElement>(null);
|
const keyboardRef = useRef<HTMLDivElement>(null);
|
||||||
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore();
|
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } =
|
||||||
const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
useUiStore();
|
||||||
|
const { keysDownState, isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } =
|
||||||
|
useHidStore();
|
||||||
const { handleKeyPress, executeMacro } = useKeyboard();
|
const { handleKeyPress, executeMacro } = useKeyboard();
|
||||||
const { selectedKeyboard } = useKeyboardLayout();
|
const { selectedKeyboard } = useKeyboardLayout();
|
||||||
|
|
||||||
|
|
@ -44,29 +42,28 @@ function KeyboardWrapper() {
|
||||||
return selectedKeyboard.virtualKeyboard;
|
return selectedKeyboard.virtualKeyboard;
|
||||||
}, [selectedKeyboard]);
|
}, [selectedKeyboard]);
|
||||||
|
|
||||||
//const isCapsLockActive = useMemo(() => {
|
const { isShiftActive } = useMemo(() => {
|
||||||
// return (keyboardLedState.caps_lock);
|
|
||||||
//}, [keyboardLedState]);
|
|
||||||
|
|
||||||
const { isShiftActive, /*isControlActive, isAltActive, isMetaActive, isAltGrActive*/ } = useMemo(() => {
|
|
||||||
return decodeModifiers(keysDownState.modifier);
|
return decodeModifiers(keysDownState.modifier);
|
||||||
}, [keysDownState]);
|
}, [keysDownState]);
|
||||||
|
|
||||||
const mainLayoutName = useMemo(() => {
|
const mainLayoutName = useMemo(() => {
|
||||||
const layoutName = isShiftActive ? "shift": "default";
|
return isShiftActive ? "shift" : "default";
|
||||||
return layoutName;
|
|
||||||
}, [isShiftActive]);
|
}, [isShiftActive]);
|
||||||
|
|
||||||
const keyNamesForDownKeys = useMemo(() => {
|
const keyNamesForDownKeys = useMemo(() => {
|
||||||
const activeModifierMask = keysDownState.modifier || 0;
|
const activeModifierMask = keysDownState.modifier || 0;
|
||||||
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
const modifierNames = Object.entries(modifiers)
|
||||||
|
.filter(([_, mask]) => (activeModifierMask & mask) !== 0)
|
||||||
|
.map(([name, _]) => name);
|
||||||
|
|
||||||
const keysDown = keysDownState.keys || [];
|
const keysDown = keysDownState.keys || [];
|
||||||
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
const keyNames = Object.entries(keys)
|
||||||
|
.filter(([_, value]) => keysDown.includes(value))
|
||||||
|
.map(([name, _]) => name);
|
||||||
|
|
||||||
return [...modifierNames,...keyNames, ' ']; // we have to have at least one space to avoid keyboard whining
|
return [...modifierNames, ...keyNames, " "]; // we have to have at least one space to avoid keyboard whining
|
||||||
}, [keysDownState]);
|
}, [keysDownState]);
|
||||||
|
|
||||||
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
||||||
if (!keyboardRef.current) return;
|
if (!keyboardRef.current) return;
|
||||||
if (e instanceof TouchEvent && e.touches.length > 1) return;
|
if (e instanceof TouchEvent && e.touches.length > 1) return;
|
||||||
|
|
@ -110,6 +107,9 @@ function KeyboardWrapper() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Is the keyboard detached or attached?
|
||||||
|
if (isAttachedVirtualKeyboardVisible) return;
|
||||||
|
|
||||||
const handle = keyboardRef.current;
|
const handle = keyboardRef.current;
|
||||||
if (handle) {
|
if (handle) {
|
||||||
handle.addEventListener("touchstart", startDrag);
|
handle.addEventListener("touchstart", startDrag);
|
||||||
|
|
@ -134,15 +134,12 @@ function KeyboardWrapper() {
|
||||||
document.removeEventListener("mousemove", onDrag);
|
document.removeEventListener("mousemove", onDrag);
|
||||||
document.removeEventListener("touchmove", onDrag);
|
document.removeEventListener("touchmove", onDrag);
|
||||||
};
|
};
|
||||||
}, [endDrag, onDrag, startDrag]);
|
}, [isAttachedVirtualKeyboardVisible, endDrag, onDrag, startDrag]);
|
||||||
|
|
||||||
const onKeyUp = useCallback(
|
const onKeyUp = useCallback(async (_: string, e: MouseEvent | undefined) => {
|
||||||
async (_: string, e: MouseEvent | undefined) => {
|
e?.preventDefault();
|
||||||
e?.preventDefault();
|
e?.stopPropagation();
|
||||||
e?.stopPropagation();
|
}, []);
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
async (key: string, e: MouseEvent | undefined) => {
|
async (key: string, e: MouseEvent | undefined) => {
|
||||||
|
|
@ -151,24 +148,30 @@ function KeyboardWrapper() {
|
||||||
|
|
||||||
// handle the fake key-macros we have defined for common combinations
|
// handle the fake key-macros we have defined for common combinations
|
||||||
if (key === "CtrlAltDelete") {
|
if (key === "CtrlAltDelete") {
|
||||||
await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
await executeMacro([
|
||||||
|
{ keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 },
|
||||||
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "AltMetaEscape") {
|
if (key === "AltMetaEscape") {
|
||||||
await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]);
|
await executeMacro([
|
||||||
|
{ keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 },
|
||||||
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "CtrlAltBackspace") {
|
if (key === "CtrlAltBackspace") {
|
||||||
await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
await executeMacro([
|
||||||
|
{ keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 },
|
||||||
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer)
|
// if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer)
|
||||||
if (latchingKeys.includes(key)) {
|
if (latchingKeys.includes(key)) {
|
||||||
console.debug(`Latching key pressed: ${key} sending down and delayed up pair`);
|
console.debug(`Latching key pressed: ${key} sending down and delayed up pair`);
|
||||||
handleKeyPress(keys[key], true)
|
handleKeyPress(keys[key], true);
|
||||||
setTimeout(() => handleKeyPress(keys[key], false), 100);
|
setTimeout(() => handleKeyPress(keys[key], false), 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -176,8 +179,10 @@ function KeyboardWrapper() {
|
||||||
// if they press any of the dynamic keys, we send a keypress down event but we don't release it until they click it again
|
// if they press any of the dynamic keys, we send a keypress down event but we don't release it until they click it again
|
||||||
if (Object.keys(modifiers).includes(key)) {
|
if (Object.keys(modifiers).includes(key)) {
|
||||||
const currentlyDown = keyNamesForDownKeys.includes(key);
|
const currentlyDown = keyNamesForDownKeys.includes(key);
|
||||||
console.debug(`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`);
|
console.debug(
|
||||||
handleKeyPress(keys[key], !currentlyDown)
|
`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`,
|
||||||
|
);
|
||||||
|
handleKeyPress(keys[key], !currentlyDown);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +216,7 @@ function KeyboardWrapper() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
!isAttachedVirtualKeyboardVisible
|
!isAttachedVirtualKeyboardVisible
|
||||||
? "fixed left-0 top-0 z-50 select-none"
|
? "fixed top-0 left-0 z-10 select-none"
|
||||||
: "relative",
|
: "relative",
|
||||||
)}
|
)}
|
||||||
ref={keyboardRef}
|
ref={keyboardRef}
|
||||||
|
|
@ -224,9 +229,10 @@ function KeyboardWrapper() {
|
||||||
<Card
|
<Card
|
||||||
className={cx("overflow-hidden", {
|
className={cx("overflow-hidden", {
|
||||||
"rounded-none": isAttachedVirtualKeyboardVisible,
|
"rounded-none": isAttachedVirtualKeyboardVisible,
|
||||||
|
"keyboard-detached": !isAttachedVirtualKeyboardVisible,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center border-b border-b-slate-800/30 bg-white px-2 py-1 dark:border-b-slate-300/20 dark:bg-slate-800">
|
<div className="flex items-center justify-center border-b border-b-slate-800/30 bg-white px-2 py-4 dark:border-b-slate-300/20 dark:bg-slate-800">
|
||||||
<div className="absolute left-2 flex items-center gap-x-2">
|
<div className="absolute left-2 flex items-center gap-x-2">
|
||||||
{isAttachedVirtualKeyboardVisible ? (
|
{isAttachedVirtualKeyboardVisible ? (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -240,15 +246,25 @@ function KeyboardWrapper() {
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Attach"
|
text="Attach"
|
||||||
LeadingIcon={AttachIcon}
|
|
||||||
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="select-none self-center font-sans text-[12px] text-slate-700 dark:text-slate-300">
|
<h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300">
|
||||||
Virtual Keyboard
|
Virtual Keyboard
|
||||||
</h2>
|
</h2>
|
||||||
<div className="absolute right-2">
|
<div className="absolute right-2 flex items-center gap-x-2">
|
||||||
|
<div className="hidden md:flex gap-x-2 items-center">
|
||||||
|
<LinkButton
|
||||||
|
size="XS"
|
||||||
|
to="settings/keyboard"
|
||||||
|
theme="light"
|
||||||
|
text={selectedKeyboard.name}
|
||||||
|
LeadingIcon={LuKeyboard}
|
||||||
|
/>
|
||||||
|
<div className="h-[20px] w-px bg-slate-800/20 dark:bg-slate-200/20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
|
|
@ -317,7 +333,7 @@ function KeyboardWrapper() {
|
||||||
stopMouseUpPropagation={true}
|
stopMouseUpPropagation={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ /* TODO add optional number pad */ }
|
{/* TODO add optional number pad */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,27 @@
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||||
import { useMemo, forwardRef, useEffect, useCallback } from "react";
|
import { forwardRef, useEffect, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
LuArrowUpFromLine,
|
|
||||||
LuCheckCheck,
|
|
||||||
LuLink,
|
LuLink,
|
||||||
LuPlus,
|
LuPlus,
|
||||||
LuRadioReceiver,
|
LuRadioReceiver,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { formatters } from "@/utils";
|
import { formatters } from "@/utils";
|
||||||
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
|
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const { diskDataChannelStats } = useRTCStore();
|
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
||||||
useMountMediaStore();
|
useMountMediaStore();
|
||||||
|
|
||||||
const bytesSentPerSecond = useMemo(() => {
|
|
||||||
if (diskDataChannelStats.size < 2) return null;
|
|
||||||
|
|
||||||
const secondLastItem =
|
|
||||||
Array.from(diskDataChannelStats)[diskDataChannelStats.size - 2];
|
|
||||||
const lastItem = Array.from(diskDataChannelStats)[diskDataChannelStats.size - 1];
|
|
||||||
|
|
||||||
if (!secondLastItem || !lastItem) return 0;
|
|
||||||
|
|
||||||
const lastTime = lastItem[0];
|
|
||||||
const secondLastTime = secondLastItem[0];
|
|
||||||
const timeDelta = lastTime - secondLastTime;
|
|
||||||
|
|
||||||
const lastBytesSent = lastItem[1].bytesSent;
|
|
||||||
const secondLastBytesSent = secondLastItem[1].bytesSent;
|
|
||||||
const bytesDelta = lastBytesSent - secondLastBytesSent;
|
|
||||||
|
|
||||||
return bytesDelta / timeDelta;
|
|
||||||
}, [diskDataChannelStats]);
|
|
||||||
|
|
||||||
const syncRemoteVirtualMediaState = useCallback(() => {
|
const syncRemoteVirtualMediaState = useCallback(() => {
|
||||||
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
||||||
if ("error" in response) {
|
if ("error" in response) {
|
||||||
|
|
@ -94,42 +70,6 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const { source, filename, size, url, path } = remoteVirtualMediaState;
|
const { source, filename, size, url, path } = remoteVirtualMediaState;
|
||||||
|
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case "WebRTC":
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<LuCheckCheck className="h-5 text-green-500" />
|
|
||||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
|
||||||
Streaming from Browser
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<Card className="w-auto px-2 py-1">
|
|
||||||
<div className="w-full truncate text-sm text-black dark:text-white">
|
|
||||||
{formatters.truncateMiddle(filename, 50)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="my-2 flex flex-col items-center gap-y-2">
|
|
||||||
<div className="w-full text-sm text-slate-900 dark:text-slate-100">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span>{formatters.bytes(size ?? 0)}</span>
|
|
||||||
<div className="flex items-center gap-x-1">
|
|
||||||
<LuArrowUpFromLine
|
|
||||||
className="h-4 text-blue-700 dark:text-blue-500"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{bytesSentPerSecond !== null
|
|
||||||
? `${formatters.bytes(bytesSentPerSecond)}/s`
|
|
||||||
: "N/A"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case "HTTP":
|
case "HTTP":
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
|
|
@ -202,18 +142,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
description="Mount an image to boot from or install an operating system."
|
description="Mount an image to boot from or install an operating system."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{remoteVirtualMediaState?.source === "WebRTC" ? (
|
<div
|
||||||
<Card>
|
|
||||||
<div className="flex items-center gap-x-1.5 px-2.5 py-2 text-sm">
|
|
||||||
<ExclamationTriangleIcon className="h-4 text-yellow-500" />
|
|
||||||
<div className="flex w-full items-center text-black">
|
|
||||||
<div>Closing this tab will unmount the image</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="animate-fadeIn opacity-0 space-y-2"
|
className="animate-fadeIn opacity-0 space-y-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,6 @@ export interface RTCState {
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
||||||
rpcDataChannel: RTCDataChannel | null;
|
rpcDataChannel: RTCDataChannel | null;
|
||||||
|
|
||||||
diskChannel: RTCDataChannel | null;
|
|
||||||
setDiskChannel: (channel: RTCDataChannel) => void;
|
|
||||||
|
|
||||||
peerConnectionState: RTCPeerConnectionState | null;
|
peerConnectionState: RTCPeerConnectionState | null;
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
||||||
|
|
||||||
|
|
@ -160,9 +157,6 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
peerConnectionState: null,
|
peerConnectionState: null,
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
||||||
|
|
||||||
diskChannel: null,
|
|
||||||
setDiskChannel: (channel: RTCDataChannel) => set({ diskChannel: channel }),
|
|
||||||
|
|
||||||
mediaStream: null,
|
mediaStream: null,
|
||||||
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
||||||
|
|
||||||
|
|
@ -381,7 +375,7 @@ export const useSettingsStore = create(
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface RemoteVirtualMediaState {
|
export interface RemoteVirtualMediaState {
|
||||||
source: "WebRTC" | "HTTP" | "Storage" | null;
|
source: "HTTP" | "Storage" | null;
|
||||||
mode: "CDROM" | "Disk" | null;
|
mode: "CDROM" | "Disk" | null;
|
||||||
filename: string | null;
|
filename: string | null;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
|
|
@ -390,13 +384,10 @@ export interface RemoteVirtualMediaState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MountMediaState {
|
export interface MountMediaState {
|
||||||
localFile: File | null;
|
|
||||||
setLocalFile: (file: MountMediaState["localFile"]) => void;
|
|
||||||
|
|
||||||
remoteVirtualMediaState: RemoteVirtualMediaState | null;
|
remoteVirtualMediaState: RemoteVirtualMediaState | null;
|
||||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void;
|
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void;
|
||||||
|
|
||||||
modalView: "mode" | "browser" | "url" | "device" | "upload" | "error" | null;
|
modalView: "mode" | "url" | "device" | "upload" | "error" | null;
|
||||||
setModalView: (view: MountMediaState["modalView"]) => void;
|
setModalView: (view: MountMediaState["modalView"]) => void;
|
||||||
|
|
||||||
isMountMediaDialogOpen: boolean;
|
isMountMediaDialogOpen: boolean;
|
||||||
|
|
@ -410,9 +401,6 @@ export interface MountMediaState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMountMediaStore = create<MountMediaState>(set => ({
|
export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
localFile: null,
|
|
||||||
setLocalFile: (file: MountMediaState["localFile"]) => set({ localFile: file }),
|
|
||||||
|
|
||||||
remoteVirtualMediaState: null,
|
remoteVirtualMediaState: null,
|
||||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useNavigate, useParams, NavigateOptions } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import type { NavigateOptions } from "react-router";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,20 @@ video::-webkit-media-controls {
|
||||||
@apply mr-[2px]! md:mr-[5px]!;
|
@apply mr-[2px]! md:mr-[5px]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reduce font size for selected keys when keyboard is detached */
|
||||||
|
.keyboard-detached .simple-keyboard-main.simple-keyboard {
|
||||||
|
min-width: calc(14 * 7ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-detached .simple-keyboard.hg-theme-default div.hg-button {
|
||||||
|
text-wrap: auto;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 6ch;
|
||||||
|
}
|
||||||
|
.keyboard-detached .simple-keyboard.hg-theme-default .hg-button span {
|
||||||
|
font-size: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide the scrollbar by setting the scrollbar color to the background color */
|
/* Hide the scrollbar by setting the scrollbar color to the background color */
|
||||||
.xterm .xterm-viewport {
|
.xterm .xterm-viewport {
|
||||||
scrollbar-color: var(--color-gray-900) #002b36;
|
scrollbar-color: var(--color-gray-900) #002b36;
|
||||||
|
|
|
||||||
|
|
@ -144,33 +144,33 @@ export const keyDisplayMap: Record<string, string> = {
|
||||||
AltMetaEscape: "Alt + Meta + Escape",
|
AltMetaEscape: "Alt + Meta + Escape",
|
||||||
CtrlAltBackspace: "Ctrl + Alt + Backspace",
|
CtrlAltBackspace: "Ctrl + Alt + Backspace",
|
||||||
AltGr: "AltGr",
|
AltGr: "AltGr",
|
||||||
AltLeft: "Alt",
|
AltLeft: "Alt ⌥",
|
||||||
AltRight: "Alt",
|
AltRight: "⌥ Alt",
|
||||||
ArrowDown: "↓",
|
ArrowDown: "↓",
|
||||||
ArrowLeft: "←",
|
ArrowLeft: "←",
|
||||||
ArrowRight: "→",
|
ArrowRight: "→",
|
||||||
ArrowUp: "↑",
|
ArrowUp: "↑",
|
||||||
Backspace: "Backspace",
|
Backspace: "Backspace",
|
||||||
"(Backspace)": "Backspace",
|
"(Backspace)": "Backspace",
|
||||||
CapsLock: "Caps Lock",
|
CapsLock: "Caps Lock ⇪",
|
||||||
Clear: "Clear",
|
Clear: "Clear",
|
||||||
ControlLeft: "Ctrl",
|
ControlLeft: "Ctrl ⌃",
|
||||||
ControlRight: "Ctrl",
|
ControlRight: "⌃ Ctrl",
|
||||||
Delete: "Delete",
|
Delete: "Delete ⌦",
|
||||||
End: "End",
|
End: "End",
|
||||||
Enter: "Enter",
|
Enter: "Enter",
|
||||||
Escape: "Esc",
|
Escape: "Esc",
|
||||||
Home: "Home",
|
Home: "Home",
|
||||||
Insert: "Insert",
|
Insert: "Insert",
|
||||||
Menu: "Menu",
|
Menu: "Menu",
|
||||||
MetaLeft: "Meta",
|
MetaLeft: "Meta ⌘",
|
||||||
MetaRight: "Meta",
|
MetaRight: "⌘ Meta",
|
||||||
PageDown: "PgDn",
|
PageDown: "PgDn",
|
||||||
PageUp: "PgUp",
|
PageUp: "PgUp",
|
||||||
ShiftLeft: "Shift",
|
ShiftLeft: "Shift ⇧",
|
||||||
ShiftRight: "Shift",
|
ShiftRight: "⇧ Shift",
|
||||||
Space: " ",
|
Space: " ",
|
||||||
Tab: "Tab",
|
Tab: "Tab ⇥",
|
||||||
|
|
||||||
// Letters
|
// Letters
|
||||||
KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e",
|
KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e",
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,6 @@ export const keys = {
|
||||||
Help: 0x75,
|
Help: 0x75,
|
||||||
Home: 0x4a,
|
Home: 0x4a,
|
||||||
Insert: 0x49,
|
Insert: 0x49,
|
||||||
International1: 0x87,
|
|
||||||
International2: 0x88,
|
|
||||||
International3: 0x89,
|
|
||||||
International4: 0x8a,
|
|
||||||
International5: 0x8b,
|
|
||||||
International6: 0x8c,
|
|
||||||
International7: 0x8d,
|
International7: 0x8d,
|
||||||
International8: 0x8e,
|
International8: 0x8e,
|
||||||
International9: 0x8f,
|
International9: 0x8f,
|
||||||
|
|
@ -117,14 +111,20 @@ export const keys = {
|
||||||
KeyX: 0x1b,
|
KeyX: 0x1b,
|
||||||
KeyY: 0x1c,
|
KeyY: 0x1c,
|
||||||
KeyZ: 0x1d,
|
KeyZ: 0x1d,
|
||||||
|
KeyRO: 0x87,
|
||||||
|
KatakanaHiragana: 0x88,
|
||||||
|
Yen: 0x89,
|
||||||
|
Henkan: 0x8a,
|
||||||
|
Muhenkan: 0x8b,
|
||||||
|
KPJPComma: 0x8c,
|
||||||
|
Hangeul: 0x90,
|
||||||
|
Hanja: 0x91,
|
||||||
|
Katakana: 0x92,
|
||||||
|
Hiragana: 0x93,
|
||||||
|
ZenkakuHankaku:0x94,
|
||||||
LockingCapsLock: 0x82,
|
LockingCapsLock: 0x82,
|
||||||
LockingNumLock: 0x83,
|
LockingNumLock: 0x83,
|
||||||
LockingScrollLock: 0x84,
|
LockingScrollLock: 0x84,
|
||||||
Lang1: 0x90, // Hangul/English toggle on Korean keyboards
|
|
||||||
Lang2: 0x91, // Hanja conversion on Korean keyboards
|
|
||||||
Lang3: 0x92, // Katakana on Japanese keyboards
|
|
||||||
Lang4: 0x93, // Hiragana on Japanese keyboards
|
|
||||||
Lang5: 0x94, // Zenkaku/Hankaku toggle on Japanese keyboards
|
|
||||||
Lang6: 0x95,
|
Lang6: 0x95,
|
||||||
Lang7: 0x96,
|
Lang7: 0x96,
|
||||||
Lang8: 0x97,
|
Lang8: 0x97,
|
||||||
|
|
@ -157,7 +157,7 @@ export const keys = {
|
||||||
NumpadClearEntry: 0xd9,
|
NumpadClearEntry: 0xd9,
|
||||||
NumpadColon: 0xcb,
|
NumpadColon: 0xcb,
|
||||||
NumpadComma: 0x85,
|
NumpadComma: 0x85,
|
||||||
NumpadDecimal: 0x63,
|
NumpadDecimal: 0x63, // and Delete
|
||||||
NumpadDecimalBase: 0xdc,
|
NumpadDecimalBase: 0xdc,
|
||||||
NumpadDelete: 0x63,
|
NumpadDelete: 0x63,
|
||||||
NumpadDivide: 0x54,
|
NumpadDivide: 0x54,
|
||||||
|
|
@ -211,7 +211,7 @@ export const keys = {
|
||||||
PageUp: 0x4b,
|
PageUp: 0x4b,
|
||||||
Paste: 0x7d,
|
Paste: 0x7d,
|
||||||
Pause: 0x48,
|
Pause: 0x48,
|
||||||
Period: 0x37,
|
Period: 0x37, // aka Dot
|
||||||
Power: 0x66,
|
Power: 0x66,
|
||||||
PrintScreen: 0x46,
|
PrintScreen: 0x46,
|
||||||
Prior: 0x9d,
|
Prior: 0x9d,
|
||||||
|
|
@ -226,7 +226,7 @@ export const keys = {
|
||||||
Slash: 0x38,
|
Slash: 0x38,
|
||||||
Space: 0x2c,
|
Space: 0x2c,
|
||||||
Stop: 0x78,
|
Stop: 0x78,
|
||||||
SystemRequest: 0x9a,
|
SystemRequest: 0x9a, // aka Attention
|
||||||
Tab: 0x2b,
|
Tab: 0x2b,
|
||||||
ThousandsSeparator: 0xb2,
|
ThousandsSeparator: 0xb2,
|
||||||
Tilde: 0x35,
|
Tilde: 0x35,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
redirect,
|
redirect,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
useRouteError,
|
useRouteError,
|
||||||
} from "react-router-dom";
|
} from "react-router";
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
|
|
@ -28,7 +28,7 @@ import DeviceIdRename from "@routes/devices.$id.rename";
|
||||||
import DevicesRoute from "@routes/devices";
|
import DevicesRoute from "@routes/devices";
|
||||||
import SettingsIndexRoute from "@routes/devices.$id.settings._index";
|
import SettingsIndexRoute from "@routes/devices.$id.settings._index";
|
||||||
import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index";
|
import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index";
|
||||||
const Notifications = lazy(() => import("@/notifications"));
|
import Notifications from "@/notifications";
|
||||||
const SignupRoute = lazy(() => import("@routes/signup"));
|
const SignupRoute = lazy(() => import("@routes/signup"));
|
||||||
const LoginRoute = lazy(() => import("@routes/login"));
|
const LoginRoute = lazy(() => import("@routes/login"));
|
||||||
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
|
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
function Root() {
|
function Root() {
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
import { redirect } from "react-router";
|
||||||
|
import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
|
|
||||||
import { DEVICE_API } from "@/ui.config";
|
import { DEVICE_API } from "@/ui.config";
|
||||||
|
import api from "@/api";
|
||||||
import api from "../api";
|
|
||||||
|
|
||||||
export interface CloudState {
|
export interface CloudState {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
|
@ -10,7 +10,7 @@ export interface CloudState {
|
||||||
appUrl: string;
|
appUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async ({ request }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const searchParams = url.searchParams;
|
const searchParams = url.searchParams;
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AdoptRoute() {
|
export default function AdoptRoute() {
|
||||||
return <></>;
|
return (<></>);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdoptRoute.loader = loader;
|
AdoptRoute.loader = loader;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
||||||
ActionFunctionArgs,
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
Form,
|
|
||||||
LoaderFunctionArgs,
|
|
||||||
redirect,
|
|
||||||
useActionData,
|
|
||||||
useLoaderData,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
|
@ -22,7 +16,7 @@ interface LoaderData {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const { deviceId } = Object.fromEntries(await request.formData());
|
const { deviceId } = Object.fromEntries(await request.formData());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,17 +28,17 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return { message: "There was an error renaming your device. Please try again." };
|
return { message: "There was an error deregistering your device. Please try again." };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { message: "There was an error renaming your device. Please try again." };
|
return { message: "There was an error deregistering your device. Please try again." };
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/devices");
|
return redirect("/devices");
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LuGlobe,
|
|
||||||
LuLink,
|
LuLink,
|
||||||
LuRadioReceiver,
|
LuRadioReceiver,
|
||||||
LuHardDrive,
|
|
||||||
LuCheck,
|
LuCheck,
|
||||||
LuUpload,
|
LuUpload,
|
||||||
} from "react-icons/lu";
|
} 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 { TrashIcon } from "@heroicons/react/16/solid";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import Card, { GridCard } from "@/components/Card";
|
import Card, { GridCard } from "@/components/Card";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
@ -50,7 +48,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
const {
|
const {
|
||||||
modalView,
|
modalView,
|
||||||
setModalView,
|
setModalView,
|
||||||
setLocalFile,
|
|
||||||
setRemoteVirtualMediaState,
|
setRemoteVirtualMediaState,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
|
|
@ -60,7 +57,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
const [incompleteFileName, setIncompleteFileName] = useState<string | null>(null);
|
const [incompleteFileName, setIncompleteFileName] = useState<string | null>(null);
|
||||||
const [mountInProgress, setMountInProgress] = useState(false);
|
const [mountInProgress, setMountInProgress] = useState(false);
|
||||||
function clearMountMediaState() {
|
function clearMountMediaState() {
|
||||||
setLocalFile(null);
|
|
||||||
setRemoteVirtualMediaState(null);
|
setRemoteVirtualMediaState(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,35 +127,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
clearMountMediaState();
|
clearMountMediaState();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBrowserMount(file: File, mode: RemoteVirtualMediaState["mode"]) {
|
const [selectedMode, setSelectedMode] = useState<"url" | "device">("url");
|
||||||
console.log(`Mounting ${file.name} as ${mode}`);
|
|
||||||
|
|
||||||
setMountInProgress(true);
|
|
||||||
send(
|
|
||||||
"mountWithWebRTC",
|
|
||||||
{ filename: file.name, size: file.size, mode },
|
|
||||||
async resp => {
|
|
||||||
if ("error" in resp) triggerError(resp.error.message);
|
|
||||||
|
|
||||||
clearMountMediaState();
|
|
||||||
syncRemoteVirtualMediaState()
|
|
||||||
.then(() => {
|
|
||||||
// We need to keep the local file in the store so that the browser can
|
|
||||||
// continue to stream the file to the device
|
|
||||||
setLocalFile(file);
|
|
||||||
navigate("..");
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
triggerError(err instanceof Error ? err.message : String(err));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setMountInProgress(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [selectedMode, setSelectedMode] = useState<"browser" | "url" | "device">("url");
|
|
||||||
return (
|
return (
|
||||||
<AutoHeight>
|
<AutoHeight>
|
||||||
<div
|
<div
|
||||||
|
|
@ -167,7 +135,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
"max-w-4xl": modalView === "mode",
|
"max-w-4xl": modalView === "mode",
|
||||||
"max-w-2xl": modalView === "device",
|
"max-w-2xl": modalView === "device",
|
||||||
"max-w-xl":
|
"max-w-xl":
|
||||||
modalView === "browser" ||
|
|
||||||
modalView === "url" ||
|
modalView === "url" ||
|
||||||
modalView === "upload" ||
|
modalView === "upload" ||
|
||||||
modalView === "error",
|
modalView === "error",
|
||||||
|
|
@ -194,19 +161,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{modalView === "browser" && (
|
|
||||||
<BrowserFileView
|
|
||||||
mountInProgress={mountInProgress}
|
|
||||||
onMountFile={(file, mode) => {
|
|
||||||
handleBrowserMount(file, mode);
|
|
||||||
}}
|
|
||||||
onBack={() => {
|
|
||||||
setMountInProgress(false);
|
|
||||||
setModalView("mode");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "url" && (
|
{modalView === "url" && (
|
||||||
<UrlView
|
<UrlView
|
||||||
mountInProgress={mountInProgress}
|
mountInProgress={mountInProgress}
|
||||||
|
|
@ -275,8 +229,8 @@ function ModeSelectionView({
|
||||||
setSelectedMode,
|
setSelectedMode,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
selectedMode: "browser" | "url" | "device";
|
selectedMode: "url" | "device";
|
||||||
setSelectedMode: (mode: "browser" | "url" | "device") => void;
|
setSelectedMode: (mode: "url" | "device") => void;
|
||||||
}) {
|
}) {
|
||||||
const { setModalView } = useMountMediaStore();
|
const { setModalView } = useMountMediaStore();
|
||||||
|
|
||||||
|
|
@ -292,14 +246,6 @@ function ModeSelectionView({
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
{[
|
{[
|
||||||
{
|
|
||||||
label: "Browser Mount",
|
|
||||||
value: "browser",
|
|
||||||
description: "Stream files directly from your browser",
|
|
||||||
icon: LuGlobe,
|
|
||||||
tag: "Coming Soon",
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "URL Mount",
|
label: "URL Mount",
|
||||||
value: "url",
|
value: "url",
|
||||||
|
|
@ -338,7 +284,7 @@ function ModeSelectionView({
|
||||||
<div
|
<div
|
||||||
className="relative z-50 flex flex-col items-start p-4 select-none"
|
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
disabled ? null : setSelectedMode(mode as "url" | "device")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -394,119 +340,6 @@ function ModeSelectionView({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BrowserFileView({
|
|
||||||
onMountFile,
|
|
||||||
onBack,
|
|
||||||
mountInProgress,
|
|
||||||
}: {
|
|
||||||
onBack: () => void;
|
|
||||||
onMountFile: (file: File, mode: RemoteVirtualMediaState["mode"]) => void;
|
|
||||||
mountInProgress: boolean;
|
|
||||||
}) {
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
||||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = event.target.files?.[0] || null;
|
|
||||||
setSelectedFile(file);
|
|
||||||
|
|
||||||
if (file?.name.endsWith(".iso")) {
|
|
||||||
setUsbMode("CDROM");
|
|
||||||
} else if (file?.name.endsWith(".img")) {
|
|
||||||
setUsbMode("Disk");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMount = () => {
|
|
||||||
if (selectedFile) {
|
|
||||||
console.log(`Mounting ${selectedFile.name} as ${setUsbMode}`);
|
|
||||||
onMountFile(selectedFile, usbMode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full space-y-4">
|
|
||||||
<ViewHeader
|
|
||||||
title="Mount from Browser"
|
|
||||||
description="Select an image file to mount"
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div
|
|
||||||
onClick={() => document.getElementById("file-upload")?.click()}
|
|
||||||
className="block cursor-pointer select-none"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="group animate-fadeIn opacity-0"
|
|
||||||
style={{
|
|
||||||
animationDuration: "0.7s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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 leading-none font-semibold">
|
|
||||||
{formatters.truncateMiddle(selectedFile.name, 40)}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs leading-none text-slate-700">
|
|
||||||
{formatters.bytes(selectedFile.size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700" />
|
|
||||||
<h3 className="text-sm leading-none font-semibold">
|
|
||||||
Click to select a file
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs leading-none text-slate-700">
|
|
||||||
Supported formats: ISO, IMG
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="file-upload"
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
className="hidden"
|
|
||||||
accept=".iso, .img"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
|
||||||
style={{
|
|
||||||
animationDuration: "0.7s",
|
|
||||||
animationDelay: "0.1s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Fieldset disabled={!selectedFile}>
|
|
||||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
|
||||||
</Fieldset>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="MD" theme="blank" text="Back" onClick={onBack} />
|
|
||||||
<Button
|
|
||||||
size="MD"
|
|
||||||
theme="primary"
|
|
||||||
text="Mount File"
|
|
||||||
onClick={handleMount}
|
|
||||||
disabled={!selectedFile || mountInProgress}
|
|
||||||
loading={mountInProgress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UrlView({
|
function UrlView({
|
||||||
onBack,
|
onBack,
|
||||||
onMount,
|
onMount,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
import { useNavigate, useOutletContext } from "react-router";
|
||||||
|
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@/components/Card";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
||||||
ActionFunctionArgs,
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
Form,
|
|
||||||
LoaderFunctionArgs,
|
|
||||||
redirect,
|
|
||||||
useActionData,
|
|
||||||
useLoaderData,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
|
@ -25,7 +19,7 @@ interface LoaderData {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async ({ params, request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const { name } = Object.fromEntries(await request.formData());
|
const { name } = Object.fromEntries(await request.formData());
|
||||||
|
|
||||||
|
|
@ -48,7 +42,7 @@ const action = async ({ params, request }: ActionFunctionArgs) => {
|
||||||
return redirect("/devices");
|
return redirect("/devices");
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
import { redirect } from "react-router";
|
||||||
|
import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
|
|
||||||
import { getDeviceUiPath } from "../hooks/useAppNavigation";
|
import { getDeviceUiPath } from "../hooks/useAppNavigation";
|
||||||
|
|
||||||
const loader = ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return redirect(getDeviceUiPath("/settings/general", params.id));
|
return redirect(getDeviceUiPath("/settings/general", params.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useLoaderData, useNavigate } from "react-router-dom";
|
import { useLoaderData, useNavigate } from "react-router";
|
||||||
|
import type { LoaderFunction } from "react-router";
|
||||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ export interface TLSState {
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
if (isOnDevice) {
|
if (isOnDevice) {
|
||||||
const status = await api
|
const status = await api
|
||||||
.GET(`${DEVICE_API}/device`)
|
.GET(`${DEVICE_API}/device`)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useLocation, useRevalidator } from "react-router-dom";
|
import { useLocation, useRevalidator } from "react-router";
|
||||||
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export default function SettingsKeyboardRoute() {
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Paste text"
|
title="Keyboard Layout"
|
||||||
description="Keyboard layout of target operating system"
|
description="Keyboard layout of target operating system"
|
||||||
>
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
|
|
@ -66,7 +66,7 @@ export default function SettingsKeyboardRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
<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.
|
The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { LuTrash2 } from "react-icons/lu";
|
import { LuTrash2 } from "react-icons/lu";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import {
|
import {
|
||||||
LuPenLine,
|
LuPenLine,
|
||||||
LuCopy,
|
LuCopy,
|
||||||
|
|
|
||||||
|
|
@ -419,7 +419,7 @@ export default function SettingsNetworkRoute() {
|
||||||
value={networkSettings.ipv6_mode}
|
value={networkSettings.ipv6_mode}
|
||||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
onChange={e => handleIpv6ModeChange(e.target.value)}
|
||||||
options={filterUnknown([
|
options={filterUnknown([
|
||||||
// { value: "disabled", label: "Disabled" },
|
{ value: "disabled", label: "Disabled" },
|
||||||
{ value: "slaac", label: "SLAAC" },
|
{ value: "slaac", label: "SLAAC" },
|
||||||
// { value: "dhcpv6", label: "DHCPv6" },
|
// { value: "dhcpv6", label: "DHCPv6" },
|
||||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
import { NavLink, Outlet, useLocation } from "react-router";
|
||||||
import {
|
import {
|
||||||
LuSettings,
|
LuSettings,
|
||||||
LuMouse,
|
LuMouse,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ const edids = [
|
||||||
"00FFFFFFFFFFFF0010AC132045393639201E0103803C22782ACD25A3574B9F270D5054A54B00714F8180A9C0D1C00101010101010101023A801871382D40582C450056502100001E000000FF00335335475132330A2020202020000000FC0044454C4C204432373231480A20000000FD00384C1E5311000A202020202020018102031AB14F90050403020716010611121513141F65030C001000023A801871382D40582C450056502100001E011D8018711C1620582C250056502100009E011D007251D01E206E28550056502100001E8C0AD08A20E02D10103E960056502100001800000000000000000000000000000000000000000000000000000000004F",
|
"00FFFFFFFFFFFF0010AC132045393639201E0103803C22782ACD25A3574B9F270D5054A54B00714F8180A9C0D1C00101010101010101023A801871382D40582C450056502100001E000000FF00335335475132330A2020202020000000FC0044454C4C204432373231480A20000000FD00384C1E5311000A202020202020018102031AB14F90050403020716010611121513141F65030C001000023A801871382D40582C450056502100001E011D8018711C1620582C250056502100009E011D007251D01E206E28550056502100001E8C0AD08A20E02D10103E960056502100001800000000000000000000000000000000000000000000000000000000004F",
|
||||||
label: "DELL D2721H, 1920x1080",
|
label: "DELL D2721H, 1920x1080",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
"00ffffffffffff0010ac0100020000000111010380221bff0a00000000000000000000adce0781800101010101010101010101010101000000ff0030303030303030303030303030000000ff0030303030303030303030303030000000fd00384c1f530b000a000000000000000000fc0044454c4c2049445241430a2020000a",
|
||||||
|
label: "DELL IDRAC EDID, 1280x1024",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const streamQualityOptions = [
|
const streamQualityOptions = [
|
||||||
|
|
@ -140,7 +145,7 @@ export default function SettingsVideoRoute() {
|
||||||
title="Video Enhancement"
|
title="Video Enhancement"
|
||||||
description="Adjust color settings to make the video output more vibrant and colorful"
|
description="Adjust color settings to make the video output more vibrant and colorful"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-4 pl-4">
|
<div className="space-y-4 pl-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Saturation"
|
title="Saturation"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
import {
|
import { Form, redirect, useActionData, useParams, useSearchParams } from "react-router";
|
||||||
ActionFunctionArgs,
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
Form,
|
|
||||||
LoaderFunctionArgs,
|
|
||||||
redirect,
|
|
||||||
useActionData,
|
|
||||||
useParams,
|
|
||||||
useSearchParams,
|
|
||||||
} from "react-router-dom";
|
|
||||||
|
|
||||||
import SimpleNavbar from "@components/SimpleNavbar";
|
import SimpleNavbar from "@components/SimpleNavbar";
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
|
|
@ -20,7 +13,7 @@ import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
|
const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|
@ -35,7 +28,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
||||||
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
|
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
|
||||||
|
|
@ -43,7 +36,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
||||||
} else {
|
} else {
|
||||||
return { error: "There was an error creating your device" };
|
return { error: "There was an error registering your device" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LoaderFunctionArgs,
|
|
||||||
Outlet,
|
Outlet,
|
||||||
Params,
|
|
||||||
redirect,
|
redirect,
|
||||||
useLoaderData,
|
useLoaderData,
|
||||||
useLocation,
|
useLocation,
|
||||||
|
|
@ -10,7 +8,8 @@ import {
|
||||||
useOutlet,
|
useOutlet,
|
||||||
useParams,
|
useParams,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
} from "react-router-dom";
|
} from "react-router";
|
||||||
|
import type { LoaderFunction, LoaderFunctionArgs, Params } from "react-router";
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
import { FocusTrap } from "focus-trap-react";
|
import { FocusTrap } from "focus-trap-react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
@ -29,7 +28,6 @@ import {
|
||||||
USBStates,
|
USBStates,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMountMediaStore,
|
|
||||||
useNetworkStateStore,
|
useNetworkStateStore,
|
||||||
User,
|
User,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
|
|
@ -113,7 +111,7 @@ const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> =>
|
||||||
return { user, iceConfig, deviceName: device.name || device.id };
|
return { user, iceConfig, deviceName: device.name || device.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -132,7 +130,6 @@ export default function KvmIdRoute() {
|
||||||
const {
|
const {
|
||||||
peerConnection, setPeerConnection,
|
peerConnection, setPeerConnection,
|
||||||
peerConnectionState, setPeerConnectionState,
|
peerConnectionState, setPeerConnectionState,
|
||||||
diskChannel, setDiskChannel,
|
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setRpcDataChannel,
|
setRpcDataChannel,
|
||||||
isTurnServerInUse, setTurnServerInUse,
|
isTurnServerInUse, setTurnServerInUse,
|
||||||
|
|
@ -484,18 +481,12 @@ export default function KvmIdRoute() {
|
||||||
setRpcDataChannel(rpcDataChannel);
|
setRpcDataChannel(rpcDataChannel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const diskDataChannel = pc.createDataChannel("disk");
|
|
||||||
diskDataChannel.onopen = () => {
|
|
||||||
setDiskChannel(diskDataChannel);
|
|
||||||
};
|
|
||||||
|
|
||||||
setPeerConnection(pc);
|
setPeerConnection(pc);
|
||||||
}, [
|
}, [
|
||||||
cleanupAndStopReconnecting,
|
cleanupAndStopReconnecting,
|
||||||
iceConfig?.iceServers,
|
iceConfig?.iceServers,
|
||||||
legacyHTTPSignaling,
|
legacyHTTPSignaling,
|
||||||
sendWebRTCSignal,
|
sendWebRTCSignal,
|
||||||
setDiskChannel,
|
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setPeerConnection,
|
setPeerConnection,
|
||||||
setPeerConnectionState,
|
setPeerConnectionState,
|
||||||
|
|
@ -719,25 +710,6 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
||||||
|
|
||||||
const { localFile } = useMountMediaStore();
|
|
||||||
useEffect(() => {
|
|
||||||
if (!diskChannel || !localFile) return;
|
|
||||||
diskChannel.onmessage = async e => {
|
|
||||||
console.debug("Received", e.data);
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
const blob = localFile.slice(data.start, data.end);
|
|
||||||
const buf = await blob.arrayBuffer();
|
|
||||||
const header = new ArrayBuffer(16);
|
|
||||||
const headerView = new DataView(header);
|
|
||||||
headerView.setBigUint64(0, BigInt(data.start), false); // start offset, big-endian
|
|
||||||
headerView.setBigUint64(8, BigInt(buf.byteLength), false); // length, big-endian
|
|
||||||
const fullData = new Uint8Array(header.byteLength + buf.byteLength);
|
|
||||||
fullData.set(new Uint8Array(header), 0);
|
|
||||||
fullData.set(new Uint8Array(buf), header.byteLength);
|
|
||||||
diskChannel.send(fullData);
|
|
||||||
};
|
|
||||||
}, [diskChannel, localFile]);
|
|
||||||
|
|
||||||
// System update
|
// System update
|
||||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useLoaderData, useRevalidator } from "react-router-dom";
|
import { useLoaderData, useRevalidator } from "react-router";
|
||||||
|
import type { LoaderFunction } from "react-router";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
|
|
@ -16,7 +17,7 @@ interface LoaderData {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
|
import { Form, redirect, useActionData } from "react-router";
|
||||||
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||||
|
|
||||||
|
|
@ -17,7 +18,7 @@ import ExtLink from "../components/ExtLink";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
@ -29,7 +30,7 @@ const loader = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
|
|
||||||
|
|
@ -86,6 +87,7 @@ export default function LoginLocalRoute() {
|
||||||
label="Password"
|
label="Password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
name="password"
|
name="password"
|
||||||
|
autoComplete="current-password"
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
autoFocus
|
autoFocus
|
||||||
error={actionData?.error}
|
error={actionData?.error}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useSearchParams } from "react-router-dom";
|
import { useLocation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
import AuthLayout from "@components/AuthLayout";
|
import AuthLayout from "@components/AuthLayout";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useSearchParams } from "react-router-dom";
|
import { useLocation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
import AuthLayout from "@components/AuthLayout";
|
import AuthLayout from "@components/AuthLayout";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
|
import { Form, redirect, useActionData } from "react-router";
|
||||||
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
|
|
@ -14,7 +15,7 @@ import api from "../api";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
@ -23,7 +24,7 @@ const loader = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const localAuthMode = formData.get("localAuthMode");
|
const localAuthMode = formData.get("localAuthMode");
|
||||||
if (!localAuthMode) return { error: "Please select an authentication mode" };
|
if (!localAuthMode) return { error: "Please select an authentication mode" };
|
||||||
|
|
@ -162,5 +163,5 @@ export default function WelcomeLocalModeRoute() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WelcomeLocalModeRoute.action = action;
|
|
||||||
WelcomeLocalModeRoute.loader = loader;
|
WelcomeLocalModeRoute.loader = loader;
|
||||||
|
WelcomeLocalModeRoute.action = action;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
|
import { Form, redirect, useActionData } from "react-router";
|
||||||
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ import api from "../api";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
@ -24,7 +25,7 @@ const loader = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
const confirmPassword = formData.get("confirmPassword");
|
const confirmPassword = formData.get("confirmPassword");
|
||||||
|
|
@ -174,5 +175,5 @@ export default function WelcomeLocalPasswordRoute() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WelcomeLocalPasswordRoute.action = action;
|
|
||||||
WelcomeLocalPasswordRoute.loader = loader;
|
WelcomeLocalPasswordRoute.loader = loader;
|
||||||
|
WelcomeLocalPasswordRoute.action = action;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { cx } from "cva";
|
import { cx } from "cva";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router";
|
||||||
|
import type { LoaderFunction } from "react-router";
|
||||||
|
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
|
|
@ -17,7 +18,7 @@ export interface DeviceStatus {
|
||||||
isSetup: boolean;
|
isSetup: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,6 @@ func setMassStorageMode(cdrom bool) error {
|
||||||
return gadget.UpdateGadgetConfig()
|
return gadget.UpdateGadgetConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func onDiskMessage(msg webrtc.DataChannelMessage) {
|
|
||||||
logger.Info().Int("len", len(msg.Data)).Msg("Disk Message")
|
|
||||||
diskReadChan <- msg.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
func mountImage(imagePath string) error {
|
func mountImage(imagePath string) error {
|
||||||
err := setMassStorageImage("")
|
err := setMassStorageImage("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -166,7 +161,6 @@ func rpcCheckMountUrl(url string) (*VirtualMediaUrlInfo, error) {
|
||||||
type VirtualMediaSource string
|
type VirtualMediaSource string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WebRTC VirtualMediaSource = "WebRTC"
|
|
||||||
HTTP VirtualMediaSource = "HTTP"
|
HTTP VirtualMediaSource = "HTTP"
|
||||||
Storage VirtualMediaSource = "Storage"
|
Storage VirtualMediaSource = "Storage"
|
||||||
)
|
)
|
||||||
|
|
@ -234,7 +228,6 @@ func getInitialVirtualMediaState() (*VirtualMediaState, error) {
|
||||||
initialState.Mode = CDROM
|
initialState.Mode = CDROM
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check if it's WebRTC or HTTP
|
|
||||||
switch diskPath {
|
switch diskPath {
|
||||||
case "":
|
case "":
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
@ -313,43 +306,6 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) error {
|
|
||||||
virtualMediaStateMutex.Lock()
|
|
||||||
if currentVirtualMediaState != nil {
|
|
||||||
virtualMediaStateMutex.Unlock()
|
|
||||||
return fmt.Errorf("another virtual media is already mounted")
|
|
||||||
}
|
|
||||||
currentVirtualMediaState = &VirtualMediaState{
|
|
||||||
Source: WebRTC,
|
|
||||||
Mode: mode,
|
|
||||||
Filename: filename,
|
|
||||||
Size: size,
|
|
||||||
}
|
|
||||||
virtualMediaStateMutex.Unlock()
|
|
||||||
|
|
||||||
if err := setMassStorageMode(mode == CDROM); err != nil {
|
|
||||||
return fmt.Errorf("failed to set mass storage mode: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState")
|
|
||||||
logger.Debug().Msg("Starting nbd device")
|
|
||||||
nbdDevice = NewNBDDevice()
|
|
||||||
err := nbdDevice.Start()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to start nbd device")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Debug().Msg("nbd device started")
|
|
||||||
//TODO: replace by polling on block device having right size
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
err = setMassStorageImage("/dev/nbd0")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Info().Msg("usb mass storage mounted")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
|
func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
|
||||||
filename, err := sanitizeFilename(filename)
|
filename, err := sanitizeFilename(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ type Session struct {
|
||||||
ControlChannel *webrtc.DataChannel
|
ControlChannel *webrtc.DataChannel
|
||||||
RPCChannel *webrtc.DataChannel
|
RPCChannel *webrtc.DataChannel
|
||||||
HidChannel *webrtc.DataChannel
|
HidChannel *webrtc.DataChannel
|
||||||
DiskChannel *webrtc.DataChannel
|
|
||||||
shouldUmountVirtualMedia bool
|
shouldUmountVirtualMedia bool
|
||||||
rpcQueue chan webrtc.DataChannelMessage
|
rpcQueue chan webrtc.DataChannelMessage
|
||||||
}
|
}
|
||||||
|
|
@ -126,9 +125,6 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
triggerVideoStateUpdate()
|
triggerVideoStateUpdate()
|
||||||
triggerUSBStateUpdate()
|
triggerUSBStateUpdate()
|
||||||
case "disk":
|
|
||||||
session.DiskChannel = d
|
|
||||||
d.OnMessage(onDiskMessage)
|
|
||||||
case "terminal":
|
case "terminal":
|
||||||
handleTerminalChannel(d)
|
handleTerminalChannel(d)
|
||||||
case "serial":
|
case "serial":
|
||||||
|
|
|
||||||