mirror of https://github.com/jetkvm/kvm.git
[WIP] Cleanup: PR Cleanup
This commit is contained in:
parent
274854b198
commit
cd87aa499c
|
@ -2,10 +2,8 @@ package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/jetkvm/kvm/internal/audio"
|
"github.com/jetkvm/kvm/internal/audio"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -30,6 +28,16 @@ func ensureAudioControlService() *audio.AudioControlService {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set up RPC callback functions for the audio package
|
||||||
|
audio.SetRPCCallbacks(
|
||||||
|
func() *audio.AudioControlService { return audioControlService },
|
||||||
|
func() audio.AudioConfig { return audioControlService.GetCurrentAudioQuality() },
|
||||||
|
func(quality audio.AudioQuality) error {
|
||||||
|
audioControlService.SetAudioQuality(quality)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return audioControlService
|
return audioControlService
|
||||||
}
|
}
|
||||||
|
@ -129,94 +137,6 @@ func GetCurrentAudioQuality() audio.AudioConfig {
|
||||||
return audioControlService.GetCurrentAudioQuality()
|
return audioControlService.GetCurrentAudioQuality()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAudioMute handles POST /audio/mute requests
|
|
||||||
func handleAudioMute(c *gin.Context) {
|
|
||||||
type muteReq struct {
|
|
||||||
Muted bool `json:"muted"`
|
|
||||||
}
|
|
||||||
var req muteReq
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, gin.H{"error": "invalid request"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if req.Muted {
|
|
||||||
err = MuteAudioOutput()
|
|
||||||
} else {
|
|
||||||
err = UnmuteAudioOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"status": "audio mute state updated",
|
|
||||||
"muted": req.Muted,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleMicrophoneStart handles POST /microphone/start requests
|
|
||||||
func handleMicrophoneStart(c *gin.Context) {
|
|
||||||
err := StartMicrophone()
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleMicrophoneStop handles POST /microphone/stop requests
|
|
||||||
func handleMicrophoneStop(c *gin.Context) {
|
|
||||||
err := StopMicrophone()
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleMicrophoneMute handles POST /microphone/mute requests
|
|
||||||
func handleMicrophoneMute(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
Muted bool `json:"muted"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if req.Muted {
|
|
||||||
err = StopMicrophone()
|
|
||||||
} else {
|
|
||||||
err = StartMicrophone()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleMicrophoneReset handles POST /microphone/reset requests
|
|
||||||
func handleMicrophoneReset(c *gin.Context) {
|
|
||||||
err := ResetMicrophone()
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSubscribeAudioEvents handles WebSocket audio event subscription
|
// handleSubscribeAudioEvents handles WebSocket audio event subscription
|
||||||
func handleSubscribeAudioEvents(connectionID string, wsCon *websocket.Conn, runCtx context.Context, l *zerolog.Logger) {
|
func handleSubscribeAudioEvents(connectionID string, wsCon *websocket.Conn, runCtx context.Context, l *zerolog.Logger) {
|
||||||
ensureAudioControlService()
|
ensureAudioControlService()
|
||||||
|
@ -228,57 +148,3 @@ func handleUnsubscribeAudioEvents(connectionID string, l *zerolog.Logger) {
|
||||||
ensureAudioControlService()
|
ensureAudioControlService()
|
||||||
audioControlService.UnsubscribeFromAudioEvents(connectionID, l)
|
audioControlService.UnsubscribeFromAudioEvents(connectionID, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAudioStatus handles GET requests for audio status
|
|
||||||
func handleAudioStatus(c *gin.Context) {
|
|
||||||
ensureAudioControlService()
|
|
||||||
|
|
||||||
status := audioControlService.GetAudioStatus()
|
|
||||||
c.JSON(200, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleAudioQuality handles GET requests for audio quality presets
|
|
||||||
func handleAudioQuality(c *gin.Context) {
|
|
||||||
presets := GetAudioQualityPresets()
|
|
||||||
current := GetCurrentAudioQuality()
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"presets": presets,
|
|
||||||
"current": current,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSetAudioQuality handles POST requests to set audio quality
|
|
||||||
func handleSetAudioQuality(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
Quality int `json:"quality"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if audio output is active before attempting quality change
|
|
||||||
// This prevents race conditions where quality changes are attempted before initialization
|
|
||||||
if !IsAudioOutputActive() {
|
|
||||||
c.JSON(503, gin.H{"error": "audio output not active - please wait for initialization to complete"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert int to AudioQuality type
|
|
||||||
quality := audio.AudioQuality(req.Quality)
|
|
||||||
|
|
||||||
// Set the audio quality using global convenience function
|
|
||||||
if err := SetAudioQuality(quality); err != nil {
|
|
||||||
c.JSON(500, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the updated configuration
|
|
||||||
current := GetCurrentAudioQuality()
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"success": true,
|
|
||||||
"config": current,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RPC wrapper functions for audio control
|
||||||
|
// These functions bridge the RPC layer to the AudioControlService
|
||||||
|
|
||||||
|
// These variables will be set by the main package to provide access to the global service
|
||||||
|
var (
|
||||||
|
getAudioControlServiceFunc func() *AudioControlService
|
||||||
|
getAudioQualityFunc func() AudioConfig
|
||||||
|
setAudioQualityFunc func(AudioQuality) error
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetRPCCallbacks sets the callback functions for RPC operations
|
||||||
|
func SetRPCCallbacks(
|
||||||
|
getService func() *AudioControlService,
|
||||||
|
getQuality func() AudioConfig,
|
||||||
|
setQuality func(AudioQuality) error,
|
||||||
|
) {
|
||||||
|
getAudioControlServiceFunc = getService
|
||||||
|
getAudioQualityFunc = getQuality
|
||||||
|
setAudioQualityFunc = setQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCAudioMute handles audio mute/unmute RPC requests
|
||||||
|
func RPCAudioMute(muted bool) error {
|
||||||
|
if getAudioControlServiceFunc == nil {
|
||||||
|
return fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
return service.MuteAudio(muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCAudioQuality handles audio quality change RPC requests
|
||||||
|
func RPCAudioQuality(quality int) (map[string]any, error) {
|
||||||
|
if getAudioQualityFunc == nil || setAudioQualityFunc == nil {
|
||||||
|
return nil, fmt.Errorf("audio quality functions not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert int to AudioQuality type
|
||||||
|
audioQuality := AudioQuality(quality)
|
||||||
|
|
||||||
|
// Get current audio quality configuration
|
||||||
|
currentConfig := getAudioQualityFunc()
|
||||||
|
|
||||||
|
// Set new quality if different
|
||||||
|
if currentConfig.Quality != audioQuality {
|
||||||
|
err := setAudioQualityFunc(audioQuality)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set audio quality: %w", err)
|
||||||
|
}
|
||||||
|
// Get updated config after setting
|
||||||
|
newConfig := getAudioQualityFunc()
|
||||||
|
return map[string]any{"config": newConfig}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return current config if no change needed
|
||||||
|
return map[string]any{"config": currentConfig}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCMicrophoneStart handles microphone start RPC requests
|
||||||
|
func RPCMicrophoneStart() error {
|
||||||
|
if getAudioControlServiceFunc == nil {
|
||||||
|
return fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
return service.StartMicrophone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCMicrophoneStop handles microphone stop RPC requests
|
||||||
|
func RPCMicrophoneStop() error {
|
||||||
|
if getAudioControlServiceFunc == nil {
|
||||||
|
return fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
return service.StopMicrophone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCAudioStatus handles audio status RPC requests (read-only)
|
||||||
|
func RPCAudioStatus() (map[string]interface{}, error) {
|
||||||
|
if getAudioControlServiceFunc == nil {
|
||||||
|
return nil, fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return nil, fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
return service.GetAudioStatus(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCAudioQualityPresets handles audio quality presets RPC requests (read-only)
|
||||||
|
func RPCAudioQualityPresets() (map[string]any, error) {
|
||||||
|
if getAudioControlServiceFunc == nil || getAudioQualityFunc == nil {
|
||||||
|
return nil, fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return nil, fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
presets := service.GetAudioQualityPresets()
|
||||||
|
current := getAudioQualityFunc()
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"presets": presets,
|
||||||
|
"current": current,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCMicrophoneStatus handles microphone status RPC requests (read-only)
|
||||||
|
func RPCMicrophoneStatus() (map[string]interface{}, error) {
|
||||||
|
if getAudioControlServiceFunc == nil {
|
||||||
|
return nil, fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return nil, fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
return service.GetMicrophoneStatus(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCMicrophoneReset handles microphone reset RPC requests
|
||||||
|
func RPCMicrophoneReset() error {
|
||||||
|
if getAudioControlServiceFunc == nil {
|
||||||
|
return fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
return service.ResetMicrophone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCMicrophoneMute handles microphone mute RPC requests
|
||||||
|
func RPCMicrophoneMute(muted bool) error {
|
||||||
|
if getAudioControlServiceFunc == nil {
|
||||||
|
return fmt.Errorf("audio control service not available")
|
||||||
|
}
|
||||||
|
service := getAudioControlServiceFunc()
|
||||||
|
if service == nil {
|
||||||
|
return fmt.Errorf("audio control service not initialized")
|
||||||
|
}
|
||||||
|
return service.MuteMicrophone(muted)
|
||||||
|
}
|
46
jsonrpc.go
46
jsonrpc.go
|
@ -1339,6 +1339,43 @@ func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audio control RPC handlers - delegated to audio package
|
||||||
|
func rpcAudioMute(muted bool) error {
|
||||||
|
return audio.RPCAudioMute(muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcAudioQuality(quality int) (map[string]any, error) {
|
||||||
|
return audio.RPCAudioQuality(quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcMicrophoneStart() error {
|
||||||
|
return audio.RPCMicrophoneStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcMicrophoneStop() error {
|
||||||
|
return audio.RPCMicrophoneStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcAudioStatus() (map[string]interface{}, error) {
|
||||||
|
return audio.RPCAudioStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcAudioQualityPresets() (map[string]any, error) {
|
||||||
|
return audio.RPCAudioQualityPresets()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcMicrophoneStatus() (map[string]interface{}, error) {
|
||||||
|
return audio.RPCMicrophoneStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcMicrophoneReset() error {
|
||||||
|
return audio.RPCMicrophoneReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcMicrophoneMute(muted bool) error {
|
||||||
|
return audio.RPCMicrophoneMute(muted)
|
||||||
|
}
|
||||||
|
|
||||||
var rpcHandlers = map[string]RPCHandler{
|
var rpcHandlers = map[string]RPCHandler{
|
||||||
"ping": {Func: rpcPing},
|
"ping": {Func: rpcPing},
|
||||||
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
||||||
|
@ -1388,6 +1425,15 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"isUpdatePending": {Func: rpcIsUpdatePending},
|
"isUpdatePending": {Func: rpcIsUpdatePending},
|
||||||
"getUsbEmulationState": {Func: rpcGetUsbEmulationState},
|
"getUsbEmulationState": {Func: rpcGetUsbEmulationState},
|
||||||
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
|
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
|
||||||
|
"audioMute": {Func: rpcAudioMute, Params: []string{"muted"}},
|
||||||
|
"audioQuality": {Func: rpcAudioQuality, Params: []string{"quality"}},
|
||||||
|
"audioStatus": {Func: rpcAudioStatus},
|
||||||
|
"audioQualityPresets": {Func: rpcAudioQualityPresets},
|
||||||
|
"microphoneStart": {Func: rpcMicrophoneStart},
|
||||||
|
"microphoneStop": {Func: rpcMicrophoneStop},
|
||||||
|
"microphoneStatus": {Func: rpcMicrophoneStatus},
|
||||||
|
"microphoneReset": {Func: rpcMicrophoneReset},
|
||||||
|
"microphoneMute": {Func: rpcMicrophoneMute, Params: []string{"muted"}},
|
||||||
"getUsbConfig": {Func: rpcGetUsbConfig},
|
"getUsbConfig": {Func: rpcGetUsbConfig},
|
||||||
"setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
|
"setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
|
||||||
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}},
|
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}},
|
||||||
|
|
|
@ -6,13 +6,13 @@ import { Fragment, useCallback, useRef } from "react";
|
||||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
import Container from "@components/Container";
|
||||||
import {
|
import {
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMountMediaStore,
|
useMountMediaStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import Container from "@components/Container";
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import PasteModal from "@/components/popovers/PasteModal";
|
import PasteModal from "@/components/popovers/PasteModal";
|
||||||
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { cva } from "@/cva.config";
|
||||||
|
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface ComboboxOption {
|
export interface ComboboxOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { GridCard } from "@/components/Card";
|
||||||
|
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
IconElm?: React.FC<{ className: string | undefined }>;
|
IconElm?: React.FC<{ className: string | undefined }>;
|
||||||
headline: string;
|
headline: string;
|
||||||
|
|
|
@ -4,20 +4,22 @@ import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/1
|
||||||
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";
|
||||||
|
|
||||||
|
import USBStateStatus from "@components/USBStateStatus";
|
||||||
|
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
||||||
import Container from "@/components/Container";
|
import Container from "@/components/Container";
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import USBStateStatus from "@components/USBStateStatus";
|
|
||||||
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
|
|
||||||
import api from "../api";
|
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
|
import api from "../api";
|
||||||
|
|
||||||
import { LinkButton } from "./Button";
|
import { LinkButton } from "./Button";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
primaryLinks?: { title: string; to: string }[];
|
primaryLinks?: { title: string; to: string }[];
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { InputFieldWithLabel } from "./InputField";
|
import { InputFieldWithLabel } from "./InputField";
|
||||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
import { SelectMenuBasic } from "./SelectMenuBasic";
|
||||||
|
|
||||||
|
|
||||||
export interface JigglerConfig {
|
export interface JigglerConfig {
|
||||||
inactivity_limit_seconds: number;
|
inactivity_limit_seconds: number;
|
||||||
jitter_percentage: number;
|
jitter_percentage: number;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import React, { JSX } from "react";
|
import React, { JSX } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import FieldLabel from "@/components/FieldLabel";
|
||||||
import { cva } from "@/cva.config";
|
import { cva } from "@/cva.config";
|
||||||
|
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type SelectMenuProps = Pick<
|
type SelectMenuProps = Pick<
|
||||||
JSX.IntrinsicElements["select"],
|
JSX.IntrinsicElements["select"],
|
||||||
"disabled" | "onChange" | "name" | "value"
|
"disabled" | "onChange" | "name" | "value"
|
||||||
|
|
|
@ -8,11 +8,13 @@ import { WebglAddon } from "@xterm/addon-webgl";
|
||||||
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||||
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
||||||
|
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
|
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
|
||||||
|
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
|
|
||||||
|
|
||||||
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
||||||
|
|
||||||
// Terminal theme configuration
|
// Terminal theme configuration
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import StatusCard from "@components/StatusCards";
|
import StatusCard from "@components/StatusCards";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
||||||
import { USBStates } from "@/hooks/stores";
|
import { USBStates } from "@/hooks/stores";
|
||||||
|
|
||||||
type StatusProps = Record<
|
type StatusProps = Record<
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||||
|
@ -6,6 +7,7 @@ import { Button } from "./Button";
|
||||||
import { GridCard } from "./Card";
|
import { GridCard } from "./Card";
|
||||||
import LoadingSpinner from "./LoadingSpinner";
|
import LoadingSpinner from "./LoadingSpinner";
|
||||||
|
|
||||||
|
|
||||||
export default function UpdateInProgressStatusCard() {
|
export default function UpdateInProgressStatusCard() {
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,10 @@ 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 { LuKeyboard } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
|
||||||
// eslint-disable-next-line import/order
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
|
||||||
|
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
|
|
||||||
|
import Card from "@components/Card";
|
||||||
|
import { Button, LinkButton } from "@components/Button";
|
||||||
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";
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { useResizeObserver } from "usehooks-ts";
|
||||||
|
|
||||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||||
import Actionbar from "@components/ActionBar";
|
import Actionbar from "@components/ActionBar";
|
||||||
import MacroBar from "@/components/MacroBar";
|
|
||||||
import InfoBar from "@components/InfoBar";
|
import InfoBar from "@components/InfoBar";
|
||||||
|
import MacroBar from "@/components/MacroBar";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
|
@ -23,6 +23,7 @@ import {
|
||||||
PointerLockBar,
|
PointerLockBar,
|
||||||
} from "./VideoOverlay";
|
} from "./VideoOverlay";
|
||||||
|
|
||||||
|
|
||||||
// Type for microphone error
|
// Type for microphone error
|
||||||
interface MicrophoneError {
|
interface MicrophoneError {
|
||||||
type: 'permission' | 'device' | 'network' | 'unknown';
|
type: 'permission' | 'device' | 'network' | 'unknown';
|
||||||
|
|
|
@ -9,6 +9,7 @@ import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "../../hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "../../hooks/useJsonRpc";
|
||||||
|
|
||||||
|
|
||||||
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
|
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
|
||||||
|
|
||||||
interface ATXState {
|
interface ATXState {
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useState } from "react";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
import FieldLabel from "@components/FieldLabel";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
|
||||||
interface DCPowerState {
|
interface DCPowerState {
|
||||||
isOn: boolean;
|
isOn: boolean;
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { useEffect, useState } from "react";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { useUiStore } from "@/hooks/stores";
|
import { useUiStore } from "@/hooks/stores";
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
|
||||||
|
|
||||||
interface SerialSettings {
|
interface SerialSettings {
|
||||||
baudRate: string;
|
baudRate: string;
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { Button } from "@components/Button";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { useAudioDevices } from "@/hooks/useAudioDevices";
|
import { useAudioDevices } from "@/hooks/useAudioDevices";
|
||||||
import { useAudioEvents } from "@/hooks/useAudioEvents";
|
import { useAudioEvents } from "@/hooks/useAudioEvents";
|
||||||
import api from "@/api";
|
import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc";
|
||||||
|
import { useRTCStore } from "@/hooks/stores";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import audioQualityService from "@/services/audioQualityService";
|
import audioQualityService from "@/services/audioQualityService";
|
||||||
|
|
||||||
|
@ -64,6 +65,17 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
||||||
isConnected: wsConnected
|
isConnected: wsConnected
|
||||||
} = useAudioEvents();
|
} = useAudioEvents();
|
||||||
|
|
||||||
|
// RPC for device communication (works both locally and via cloud)
|
||||||
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
|
// Initialize audio quality service with RPC for cloud compatibility
|
||||||
|
useEffect(() => {
|
||||||
|
if (send) {
|
||||||
|
audioQualityService.setRpcSend(send);
|
||||||
|
}
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
// WebSocket-only implementation - no fallback polling
|
// WebSocket-only implementation - no fallback polling
|
||||||
|
|
||||||
// Microphone state from props (keeping hook for legacy device operations)
|
// Microphone state from props (keeping hook for legacy device operations)
|
||||||
|
@ -146,21 +158,22 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isMuted) {
|
// Use RPC for device communication - works for both local and cloud
|
||||||
// Unmute: Start audio output process and notify backend
|
if (rpcDataChannel?.readyState !== "open") {
|
||||||
const resp = await api.POST("/audio/mute", { muted: false });
|
throw new Error("Device connection not available");
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`Failed to unmute audio: ${resp.status}`);
|
|
||||||
}
|
}
|
||||||
// WebSocket will handle the state update automatically
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
send("audioMute", { muted: !isMuted }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
reject(new Error(resp.error.message));
|
||||||
} else {
|
} else {
|
||||||
// Mute: Stop audio output process and notify backend
|
resolve();
|
||||||
const resp = await api.POST("/audio/mute", { muted: true });
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`Failed to mute audio: ${resp.status}`);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// WebSocket will handle the state update automatically
|
// WebSocket will handle the state update automatically
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Failed to toggle audio mute";
|
const errorMessage = error instanceof Error ? error.message : "Failed to toggle audio mute";
|
||||||
notifications.error(errorMessage);
|
notifications.error(errorMessage);
|
||||||
|
@ -172,13 +185,27 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
||||||
const handleQualityChange = async (quality: number) => {
|
const handleQualityChange = async (quality: number) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const resp = await api.POST("/audio/quality", { quality });
|
// Use RPC for device communication - works for both local and cloud
|
||||||
if (resp.ok) {
|
if (rpcDataChannel?.readyState !== "open") {
|
||||||
const data = await resp.json();
|
throw new Error("Device connection not available");
|
||||||
setCurrentConfig(data.config);
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// Failed to change audio quality
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
send("audioQuality", { quality }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
reject(new Error(resp.error.message));
|
||||||
|
} else {
|
||||||
|
// Update local state with response
|
||||||
|
if ("result" in resp && resp.result && typeof resp.result === 'object' && 'config' in resp.result) {
|
||||||
|
setCurrentConfig(resp.result.config as AudioConfig);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Failed to change audio quality";
|
||||||
|
notifications.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -196,17 +223,44 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Use RPC for device communication - works for both local and cloud
|
||||||
|
if (rpcDataChannel?.readyState !== "open") {
|
||||||
|
throw new Error("Device connection not available");
|
||||||
|
}
|
||||||
|
|
||||||
if (isMicrophoneActiveFromHook) {
|
if (isMicrophoneActiveFromHook) {
|
||||||
// Disable: Stop microphone subprocess AND remove WebRTC tracks
|
// Disable: Stop microphone subprocess via RPC AND remove WebRTC tracks locally
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
send("microphoneStop", {}, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
reject(new Error(resp.error.message));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also stop local WebRTC stream
|
||||||
const result = await stopMicrophone();
|
const result = await stopMicrophone();
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error?.message || "Failed to stop microphone");
|
console.warn("Local microphone stop failed:", result.error?.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Enable: Start microphone subprocess AND add WebRTC tracks
|
// Enable: Start microphone subprocess via RPC AND add WebRTC tracks locally
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
send("microphoneStart", {}, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
reject(new Error(resp.error.message));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also start local WebRTC stream
|
||||||
const result = await startMicrophone();
|
const result = await startMicrophone();
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error?.message || "Failed to start microphone");
|
throw new Error(result.error?.message || "Failed to start local microphone");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
|
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
|
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
|
||||||
import { DCPowerControl } from "@components/extensions/DCPowerControl";
|
import { DCPowerControl } from "@components/extensions/DCPowerControl";
|
||||||
import { SerialConsole } from "@components/extensions/SerialConsole";
|
import { SerialConsole } from "@components/extensions/SerialConsole";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
interface Extension {
|
interface Extension {
|
||||||
|
|
|
@ -10,9 +10,9 @@ 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 { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { formatters } from "@/utils";
|
import { formatters } from "@/utils";
|
||||||
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
||||||
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";
|
||||||
|
|
|
@ -3,17 +3,17 @@ import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { LuCornerDownLeft } from "react-icons/lu";
|
import { LuCornerDownLeft } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { GridCard } from "@components/Card";
|
||||||
|
import { InputFieldWithLabel } from "@components/InputField";
|
||||||
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { TextAreaWithLabel } from "@components/TextArea";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
|
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
|
||||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { Button } from "@components/Button";
|
|
||||||
import { GridCard } from "@components/Card";
|
|
||||||
import { InputFieldWithLabel } from "@components/InputField";
|
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|
||||||
import { TextAreaWithLabel } from "@components/TextArea";
|
|
||||||
|
|
||||||
// uint32 max value / 4
|
// uint32 max value / 4
|
||||||
const pasteMaxLength = 1073741824;
|
const pasteMaxLength = 1073741824;
|
||||||
|
|
|
@ -11,6 +11,8 @@ import EmptyStateCard from "./EmptyStateCard";
|
||||||
import DeviceList, { StoredDevice } from "./DeviceList";
|
import DeviceList, { StoredDevice } from "./DeviceList";
|
||||||
import AddDeviceForm from "./AddDeviceForm";
|
import AddDeviceForm from "./AddDeviceForm";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function WakeOnLanModal() {
|
export default function WakeOnLanModal() {
|
||||||
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
|
|
||||||
|
|
||||||
import SidebarHeader from "@/components/SidebarHeader";
|
import SidebarHeader from "@/components/SidebarHeader";
|
||||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
||||||
import { someIterable } from "@/utils";
|
import { someIterable } from "@/utils";
|
||||||
|
|
||||||
import { createChartArray, Metric } from "../Metric";
|
|
||||||
import { SettingsSectionHeader } from "../SettingsSectionHeader";
|
import { SettingsSectionHeader } from "../SettingsSectionHeader";
|
||||||
|
import { createChartArray, Metric } from "../Metric";
|
||||||
|
|
||||||
|
|
||||||
export default function ConnectionStatsSidebar() {
|
export default function ConnectionStatsSidebar() {
|
||||||
const { sidebarView, setSidebarView } = useUiStore();
|
const { sidebarView, setSidebarView } = useUiStore();
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
|
|
||||||
import { devWarn } from '../utils/debug';
|
import { devWarn } from '../utils/debug';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Define the JsonRpc types for better type checking
|
// Define the JsonRpc types for better type checking
|
||||||
interface JsonRpcResponse {
|
interface JsonRpcResponse {
|
||||||
jsonrpc: string;
|
jsonrpc: string;
|
||||||
|
|
|
@ -4,6 +4,9 @@ import useWebSocket, { ReadyState } from 'react-use-websocket';
|
||||||
import { devError, devWarn } from '../utils/debug';
|
import { devError, devWarn } from '../utils/debug';
|
||||||
import { NETWORK_CONFIG } from '../config/constants';
|
import { NETWORK_CONFIG } from '../config/constants';
|
||||||
|
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from './useJsonRpc';
|
||||||
|
import { useRTCStore } from './stores';
|
||||||
|
|
||||||
// Audio event types matching the backend
|
// Audio event types matching the backend
|
||||||
export type AudioEventType =
|
export type AudioEventType =
|
||||||
| 'audio-mute-changed'
|
| 'audio-mute-changed'
|
||||||
|
@ -63,18 +66,34 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD
|
||||||
const [audioMuted, setAudioMuted] = useState<boolean | null>(null);
|
const [audioMuted, setAudioMuted] = useState<boolean | null>(null);
|
||||||
const [microphoneState, setMicrophoneState] = useState<MicrophoneStateData | null>(null);
|
const [microphoneState, setMicrophoneState] = useState<MicrophoneStateData | null>(null);
|
||||||
|
|
||||||
// Fetch initial audio status
|
// Get RTC store and JSON RPC functionality
|
||||||
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
|
// Fetch initial audio status using RPC for cloud compatibility
|
||||||
const fetchInitialAudioStatus = useCallback(async () => {
|
const fetchInitialAudioStatus = useCallback(async () => {
|
||||||
|
// Early return if RPC data channel is not open
|
||||||
|
if (rpcDataChannel?.readyState !== "open") {
|
||||||
|
devWarn('RPC connection not available for initial audio status, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/audio/status');
|
await new Promise<void>((resolve) => {
|
||||||
if (response.ok) {
|
send("audioStatus", {}, (resp: JsonRpcResponse) => {
|
||||||
const data = await response.json();
|
if ("error" in resp) {
|
||||||
|
devError('RPC audioStatus failed:', resp.error);
|
||||||
|
} else if ("result" in resp) {
|
||||||
|
const data = resp.result as { muted: boolean };
|
||||||
setAudioMuted(data.muted);
|
setAudioMuted(data.muted);
|
||||||
}
|
}
|
||||||
|
resolve(); // Continue regardless of result
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devError('Failed to fetch initial audio status:', error);
|
devError('Failed to fetch initial audio status via RPC:', error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [rpcDataChannel?.readyState, send]);
|
||||||
|
|
||||||
// Local subscription state
|
// Local subscription state
|
||||||
const [isLocallySubscribed, setIsLocallySubscribed] = useState(false);
|
const [isLocallySubscribed, setIsLocallySubscribed] = useState(false);
|
||||||
|
@ -253,10 +272,13 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD
|
||||||
}
|
}
|
||||||
}, [readyState]);
|
}, [readyState]);
|
||||||
|
|
||||||
// Fetch initial audio status on component mount
|
// Fetch initial audio status on component mount - but only when RPC is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Only fetch when RPC data channel is open and ready
|
||||||
|
if (rpcDataChannel?.readyState === "open") {
|
||||||
fetchInitialAudioStatus();
|
fetchInitialAudioStatus();
|
||||||
}, [fetchInitialAudioStatus]);
|
}
|
||||||
|
}, [fetchInitialAudioStatus, rpcDataChannel?.readyState]);
|
||||||
|
|
||||||
// Cleanup on component unmount
|
// Cleanup on component unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {
|
||||||
unmarshalHidRpcMessage,
|
unmarshalHidRpcMessage,
|
||||||
} from "./hidRpc";
|
} from "./hidRpc";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const KEEPALIVE_MESSAGE = new KeypressKeepAliveMessage();
|
const KEEPALIVE_MESSAGE = new KeypressKeepAliveMessage();
|
||||||
|
|
||||||
interface sendMessageParams {
|
interface sendMessageParams {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useRTCStore, useSettingsStore } from "@/hooks/stores";
|
import { useRTCStore, useSettingsStore } from "@/hooks/stores";
|
||||||
import api from "@/api";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug";
|
import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug";
|
||||||
import { AUDIO_CONFIG } from "@/config/constants";
|
import { AUDIO_CONFIG } from "@/config/constants";
|
||||||
|
|
||||||
|
@ -21,9 +21,29 @@ export function useMicrophone() {
|
||||||
setMicrophoneActive,
|
setMicrophoneActive,
|
||||||
isMicrophoneMuted,
|
isMicrophoneMuted,
|
||||||
setMicrophoneMuted,
|
setMicrophoneMuted,
|
||||||
|
rpcDataChannel,
|
||||||
} = useRTCStore();
|
} = useRTCStore();
|
||||||
|
|
||||||
const { microphoneWasEnabled, setMicrophoneWasEnabled } = useSettingsStore();
|
const { microphoneWasEnabled, setMicrophoneWasEnabled } = useSettingsStore();
|
||||||
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
|
// RPC helper functions to replace HTTP API calls
|
||||||
|
const rpcMicrophoneStart = useCallback((): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (rpcDataChannel?.readyState !== "open") {
|
||||||
|
reject(new Error("Device connection not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
send("microphoneStart", {}, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
reject(new Error(resp.error.message));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [rpcDataChannel?.readyState, send]);
|
||||||
|
|
||||||
const microphoneStreamRef = useRef<MediaStream | null>(null);
|
const microphoneStreamRef = useRef<MediaStream | null>(null);
|
||||||
|
|
||||||
|
@ -60,8 +80,6 @@ export function useMicrophone() {
|
||||||
|
|
||||||
// Cleanup function to stop microphone stream
|
// Cleanup function to stop microphone stream
|
||||||
const stopMicrophoneStream = useCallback(async () => {
|
const stopMicrophoneStream = useCallback(async () => {
|
||||||
// Cleaning up microphone stream
|
|
||||||
|
|
||||||
if (microphoneStreamRef.current) {
|
if (microphoneStreamRef.current) {
|
||||||
microphoneStreamRef.current.getTracks().forEach((track: MediaStreamTrack) => {
|
microphoneStreamRef.current.getTracks().forEach((track: MediaStreamTrack) => {
|
||||||
track.stop();
|
track.stop();
|
||||||
|
@ -106,10 +124,20 @@ export function useMicrophone() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Early return if RPC data channel is not ready
|
||||||
|
if (rpcDataChannel?.readyState !== "open") {
|
||||||
|
devWarn("RPC connection not available for microphone sync, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.GET("/microphone/status", {});
|
await new Promise<void>((resolve, reject) => {
|
||||||
if (response.ok) {
|
send("microphoneStatus", {}, (resp: JsonRpcResponse) => {
|
||||||
const data = await response.json();
|
if ("error" in resp) {
|
||||||
|
devError("RPC microphone status failed:", resp.error);
|
||||||
|
reject(new Error(resp.error.message));
|
||||||
|
} else if ("result" in resp) {
|
||||||
|
const data = resp.result as { running: boolean };
|
||||||
const backendRunning = data.running;
|
const backendRunning = data.running;
|
||||||
|
|
||||||
// Only sync if there's a significant state difference and we're not in a transition
|
// Only sync if there's a significant state difference and we're not in a transition
|
||||||
|
@ -127,16 +155,21 @@ export function useMicrophone() {
|
||||||
setMicrophoneActive(false);
|
setMicrophoneActive(false);
|
||||||
// Only clean up stream if we actually have one
|
// Only clean up stream if we actually have one
|
||||||
if (microphoneStreamRef.current) {
|
if (microphoneStreamRef.current) {
|
||||||
devLog("Cleaning up orphaned stream");
|
stopMicrophoneStream();
|
||||||
await stopMicrophoneStream();
|
}
|
||||||
}
|
setMicrophoneMuted(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error("Invalid response"));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devWarn("Failed to sync microphone state:", error);
|
devError("Error syncing microphone state:", error);
|
||||||
}
|
}
|
||||||
}, [isMicrophoneActive, setMicrophoneActive, stopMicrophoneStream]);
|
}, [isMicrophoneActive, setMicrophoneActive, setMicrophoneMuted, stopMicrophoneStream, rpcDataChannel?.readyState, send]);
|
||||||
|
|
||||||
// Start microphone stream
|
// Start microphone stream
|
||||||
const startMicrophone = useCallback(async (deviceId?: string): Promise<{ success: boolean; error?: MicrophoneError }> => {
|
const startMicrophone = useCallback(async (deviceId?: string): Promise<{ success: boolean; error?: MicrophoneError }> => {
|
||||||
|
@ -169,8 +202,6 @@ export function useMicrophone() {
|
||||||
audio: audioConstraints
|
audio: audioConstraints
|
||||||
});
|
});
|
||||||
|
|
||||||
// Microphone stream created successfully
|
|
||||||
|
|
||||||
// Store the stream in both ref and store
|
// Store the stream in both ref and store
|
||||||
microphoneStreamRef.current = stream;
|
microphoneStreamRef.current = stream;
|
||||||
setMicrophoneStream(stream);
|
setMicrophoneStream(stream);
|
||||||
|
@ -291,73 +322,49 @@ export function useMicrophone() {
|
||||||
let lastError: Error | string | null = null;
|
let lastError: Error | string | null = null;
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||||
try {
|
|
||||||
// If this is a retry, first try to reset the backend microphone state
|
// If this is a retry, first try to reset the backend microphone state
|
||||||
if (attempt > 1) {
|
if (attempt > 1) {
|
||||||
devLog(`Backend start attempt ${attempt}, first trying to reset backend state...`);
|
devLog(`Backend start attempt ${attempt}, first trying to reset backend state...`);
|
||||||
try {
|
try {
|
||||||
// Try the new reset endpoint first
|
// Use RPC for reset (cloud-compatible)
|
||||||
const resetResp = await api.POST("/microphone/reset", {});
|
if (rpcDataChannel?.readyState === "open") {
|
||||||
if (resetResp.ok) {
|
await new Promise<void>((resolve) => {
|
||||||
devLog("Backend reset successful");
|
send("microphoneReset", {}, (resp: JsonRpcResponse) => {
|
||||||
} else {
|
if ("error" in resp) {
|
||||||
// Fallback to stop
|
devWarn("RPC microphone reset failed:", resp.error);
|
||||||
await api.POST("/microphone/stop", {});
|
// Try stop as fallback
|
||||||
|
send("microphoneStop", {}, (stopResp: JsonRpcResponse) => {
|
||||||
|
if ("error" in stopResp) {
|
||||||
|
devWarn("RPC microphone stop also failed:", stopResp.error);
|
||||||
}
|
}
|
||||||
|
resolve(); // Continue even if both fail
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
devLog("RPC microphone reset successful");
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
// Wait a bit for the backend to reset
|
// Wait a bit for the backend to reset
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
} else {
|
||||||
|
devWarn("RPC connection not available for reset");
|
||||||
|
}
|
||||||
} catch (resetError) {
|
} catch (resetError) {
|
||||||
devWarn("Failed to reset backend state:", resetError);
|
devWarn("Failed to reset backend state:", resetError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const backendResp = await api.POST("/microphone/start", {});
|
|
||||||
devLog(`Backend response status (attempt ${attempt}):`, backendResp.status, "ok:", backendResp.ok);
|
|
||||||
|
|
||||||
if (!backendResp.ok) {
|
|
||||||
lastError = `Backend returned status ${backendResp.status}`;
|
|
||||||
devError(`Backend microphone start failed with status: ${backendResp.status} (attempt ${attempt})`);
|
|
||||||
|
|
||||||
// For 500 errors, try again after a short delay
|
|
||||||
if (backendResp.status === 500 && attempt < 3) {
|
|
||||||
devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Success!
|
|
||||||
const responseData = await backendResp.json();
|
|
||||||
devLog("Backend response data:", responseData);
|
|
||||||
if (responseData.status === "already running") {
|
|
||||||
devInfo("Backend microphone was already running");
|
|
||||||
|
|
||||||
// If we're on the first attempt and backend says "already running",
|
|
||||||
// but frontend thinks it's not active, this might be a stuck state
|
|
||||||
if (attempt === 1 && !isMicrophoneActive) {
|
|
||||||
devWarn("Backend reports 'already running' but frontend is not active - possible stuck state");
|
|
||||||
devLog("Attempting to reset backend state and retry...");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resetResp = await api.POST("/microphone/reset", {});
|
await rpcMicrophoneStart();
|
||||||
if (resetResp.ok) {
|
devLog(`Backend RPC microphone start successful (attempt ${attempt})`);
|
||||||
devLog("Backend reset successful, retrying start...");
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
continue; // Retry the start
|
|
||||||
}
|
|
||||||
} catch (resetError) {
|
|
||||||
devWarn("Failed to reset stuck backend state:", resetError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
devLog("Backend microphone start successful");
|
|
||||||
backendSuccess = true;
|
backendSuccess = true;
|
||||||
break;
|
break; // Exit the retry loop on success
|
||||||
}
|
} catch (rpcError) {
|
||||||
} catch (error) {
|
lastError = `Backend RPC error: ${rpcError instanceof Error ? rpcError.message : 'Unknown error'}`;
|
||||||
lastError = error instanceof Error ? error : String(error);
|
devError(`Backend microphone start failed with RPC error: ${lastError} (attempt ${attempt})`);
|
||||||
devError(`Backend microphone start threw error (attempt ${attempt}):`, error);
|
|
||||||
|
|
||||||
// For network errors, try again after a short delay
|
// For RPC errors, try again after a short delay
|
||||||
if (attempt < 3) {
|
if (attempt < 3) {
|
||||||
devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`);
|
devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`);
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
@ -414,8 +421,6 @@ export function useMicrophone() {
|
||||||
setIsStarting(false);
|
setIsStarting(false);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Failed to start microphone
|
|
||||||
|
|
||||||
let micError: MicrophoneError;
|
let micError: MicrophoneError;
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
|
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
|
||||||
|
@ -446,7 +451,7 @@ export function useMicrophone() {
|
||||||
setIsStarting(false);
|
setIsStarting(false);
|
||||||
return { success: false, error: micError };
|
return { success: false, error: micError };
|
||||||
}
|
}
|
||||||
}, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, stopMicrophoneStream, isMicrophoneActive, isMicrophoneMuted, microphoneStream, isStarting, isStopping, isToggling]);
|
}, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, stopMicrophoneStream, isMicrophoneActive, isMicrophoneMuted, microphoneStream, isStarting, isStopping, isToggling, rpcMicrophoneStart, rpcDataChannel?.readyState, send]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -463,10 +468,22 @@ export function useMicrophone() {
|
||||||
// First stop the stream
|
// First stop the stream
|
||||||
await stopMicrophoneStream();
|
await stopMicrophoneStream();
|
||||||
|
|
||||||
// Then notify backend that microphone is stopped
|
// Then notify backend that microphone is stopped using RPC
|
||||||
try {
|
try {
|
||||||
await api.POST("/microphone/stop", {});
|
if (rpcDataChannel?.readyState === "open") {
|
||||||
devLog("Backend notified about microphone stop");
|
await new Promise<void>((resolve) => {
|
||||||
|
send("microphoneStop", {}, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
devWarn("RPC microphone stop failed:", resp.error);
|
||||||
|
} else {
|
||||||
|
devLog("Backend notified about microphone stop via RPC");
|
||||||
|
}
|
||||||
|
resolve(); // Continue regardless of result
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
devWarn("RPC connection not available for microphone stop");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devWarn("Failed to notify backend about microphone stop:", error);
|
devWarn("Failed to notify backend about microphone stop:", error);
|
||||||
}
|
}
|
||||||
|
@ -494,7 +511,7 @@ export function useMicrophone() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [stopMicrophoneStream, syncMicrophoneState, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, isStarting, isStopping, isToggling]);
|
}, [stopMicrophoneStream, syncMicrophoneState, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, isStarting, isStopping, isToggling, rpcDataChannel?.readyState, send]);
|
||||||
|
|
||||||
// Toggle microphone mute
|
// Toggle microphone mute
|
||||||
const toggleMicrophoneMute = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => {
|
const toggleMicrophoneMute = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => {
|
||||||
|
@ -569,9 +586,22 @@ export function useMicrophone() {
|
||||||
|
|
||||||
setMicrophoneMuted(newMutedState);
|
setMicrophoneMuted(newMutedState);
|
||||||
|
|
||||||
// Notify backend about mute state
|
// Notify backend about mute state using RPC
|
||||||
try {
|
try {
|
||||||
await api.POST("/microphone/mute", { muted: newMutedState });
|
if (rpcDataChannel?.readyState === "open") {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
send("microphoneMute", { muted: newMutedState }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
devWarn("RPC microphone mute failed:", resp.error);
|
||||||
|
} else {
|
||||||
|
devLog("Backend notified about microphone mute via RPC");
|
||||||
|
}
|
||||||
|
resolve(); // Continue regardless of result
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
devWarn("RPC connection not available for microphone mute");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devWarn("Failed to notify backend about microphone mute:", error);
|
devWarn("Failed to notify backend about microphone mute:", error);
|
||||||
}
|
}
|
||||||
|
@ -589,7 +619,7 @@ export function useMicrophone() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [microphoneStream, isMicrophoneActive, isMicrophoneMuted, setMicrophoneMuted, isStarting, isStopping, isToggling]);
|
}, [microphoneStream, isMicrophoneActive, isMicrophoneMuted, setMicrophoneMuted, isStarting, isStopping, isToggling, rpcDataChannel?.readyState, send]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -612,6 +642,12 @@ export function useMicrophone() {
|
||||||
// Sync state on mount and auto-restore microphone if it was enabled before page reload
|
// Sync state on mount and auto-restore microphone if it was enabled before page reload
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const autoRestoreMicrophone = async () => {
|
const autoRestoreMicrophone = async () => {
|
||||||
|
// Wait for RPC connection to be ready before attempting any operations
|
||||||
|
if (rpcDataChannel?.readyState !== "open") {
|
||||||
|
devLog("RPC connection not ready for microphone auto-restore, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// First sync the current state
|
// First sync the current state
|
||||||
await syncMicrophoneState();
|
await syncMicrophoneState();
|
||||||
|
|
||||||
|
@ -631,8 +667,10 @@ export function useMicrophone() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
autoRestoreMicrophone();
|
// Add a delay to ensure RTC connection is fully established
|
||||||
}, [syncMicrophoneState, microphoneWasEnabled, isMicrophoneActive, peerConnection, startMicrophone]);
|
const timer = setTimeout(autoRestoreMicrophone, 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [syncMicrophoneState, microphoneWasEnabled, isMicrophoneActive, peerConnection, startMicrophone, rpcDataChannel?.readyState]);
|
||||||
|
|
||||||
// Cleanup on unmount - use ref to avoid dependency on stopMicrophoneStream
|
// Cleanup on unmount - use ref to avoid dependency on stopMicrophoneStream
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -10,9 +10,6 @@ import {
|
||||||
} from "react-router";
|
} 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 api from "@/api";
|
|
||||||
import Root from "@/root";
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import EmptyCard from "@components/EmptyCard";
|
import EmptyCard from "@components/EmptyCard";
|
||||||
import NotFoundPage from "@components/NotFoundPage";
|
import NotFoundPage from "@components/NotFoundPage";
|
||||||
|
@ -28,6 +25,9 @@ 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";
|
||||||
|
import Root from "@/root";
|
||||||
|
import api from "@/api";
|
||||||
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
import Notifications from "@/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"));
|
||||||
|
|
|
@ -6,9 +6,9 @@ import { Button, LinkButton } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { CardHeader } from "@components/CardHeader";
|
import { CardHeader } from "@components/CardHeader";
|
||||||
import DashboardNavbar from "@components/Header";
|
import DashboardNavbar from "@components/Header";
|
||||||
|
import Fieldset from "@components/Fieldset";
|
||||||
import { User } from "@/hooks/stores";
|
import { User } from "@/hooks/stores";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import Fieldset from "@components/Fieldset";
|
|
||||||
import { CLOUD_API } from "@/ui.config";
|
import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
|
|
|
@ -9,12 +9,12 @@ import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/sol
|
||||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import Card, { GridCard } from "@/components/Card";
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
import AutoHeight from "@components/AutoHeight";
|
||||||
|
import Card, { GridCard } from "@/components/Card";
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import { formatters } from "@/utils";
|
import { formatters } from "@/utils";
|
||||||
import AutoHeight from "@components/AutoHeight";
|
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
import DebianIcon from "@/assets/debian-icon.png";
|
import DebianIcon from "@/assets/debian-icon.png";
|
||||||
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
||||||
|
@ -25,16 +25,17 @@ import NetBootIcon from "@/assets/netboot-icon.svg";
|
||||||
import Fieldset from "@/components/Fieldset";
|
import Fieldset from "@/components/Fieldset";
|
||||||
import { DEVICE_API } from "@/ui.config";
|
import { DEVICE_API } from "@/ui.config";
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
|
||||||
import notifications from "../notifications";
|
|
||||||
import { isOnDevice } from "../main";
|
|
||||||
import { cx } from "../cva.config";
|
|
||||||
import {
|
import {
|
||||||
MountMediaState,
|
MountMediaState,
|
||||||
RemoteVirtualMediaState,
|
RemoteVirtualMediaState,
|
||||||
useMountMediaStore,
|
useMountMediaStore,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
} from "../hooks/stores";
|
} from "../hooks/stores";
|
||||||
|
import { cx } from "../cva.config";
|
||||||
|
import { isOnDevice } from "../main";
|
||||||
|
import notifications from "../notifications";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
||||||
|
|
||||||
|
|
||||||
export default function MountRoute() {
|
export default function MountRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNavigate, useOutletContext } from "react-router";
|
import { useNavigate, useOutletContext } from "react-router";
|
||||||
|
|
||||||
import { GridCard } from "@/components/Card";
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
import LogoBlue from "@/assets/logo-blue.svg";
|
import LogoBlue from "@/assets/logo-blue.svg";
|
||||||
import LogoWhite from "@/assets/logo-white.svg";
|
import LogoWhite from "@/assets/logo-white.svg";
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,14 @@ import Card from "@components/Card";
|
||||||
import { CardHeader } from "@components/CardHeader";
|
import { CardHeader } from "@components/CardHeader";
|
||||||
import { InputFieldWithLabel } from "@components/InputField";
|
import { InputFieldWithLabel } from "@components/InputField";
|
||||||
import DashboardNavbar from "@components/Header";
|
import DashboardNavbar from "@components/Header";
|
||||||
|
import Fieldset from "@components/Fieldset";
|
||||||
import { User } from "@/hooks/stores";
|
import { User } from "@/hooks/stores";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import Fieldset from "@components/Fieldset";
|
|
||||||
import { CLOUD_API } from "@/ui.config";
|
import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
device: { id: string; name: string; user: { googleId: string } };
|
device: { id: string; name: string; user: { googleId: string } };
|
||||||
user: User;
|
user: User;
|
||||||
|
|
|
@ -3,8 +3,9 @@ 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";
|
||||||
|
|
||||||
import api from "@/api";
|
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { TextAreaWithLabel } from "@components/TextArea";
|
||||||
|
import api from "@/api";
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@/components/Card";
|
||||||
import { Button, LinkButton } from "@/components/Button";
|
import { Button, LinkButton } from "@/components/Button";
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
@ -15,11 +16,12 @@ import notifications from "@/notifications";
|
||||||
import { DEVICE_API } from "@/ui.config";
|
import { DEVICE_API } from "@/ui.config";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { isOnDevice } from "@/main";
|
import { isOnDevice } from "@/main";
|
||||||
import { TextAreaWithLabel } from "@components/TextArea";
|
|
||||||
|
|
||||||
import { LocalDevice } from "./devices.$id";
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
|
||||||
import { CloudState } from "./adopt";
|
import { CloudState } from "./adopt";
|
||||||
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
import { LocalDevice } from "./devices.$id";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface TLSState {
|
export interface TLSState {
|
||||||
mode: "self-signed" | "custom" | "disabled";
|
mode: "self-signed" | "custom" | "disabled";
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
import { useState , useEffect } from "react";
|
import { useState , useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
|
||||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||||
|
@ -12,6 +13,7 @@ import { useDeviceStore } from "../hooks/stores";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
|
|
||||||
export default function SettingsGeneralRoute() {
|
export default function SettingsGeneralRoute() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
|
||||||
export default function SettingsGeneralRebootRoute() {
|
export default function SettingsGeneralRebootRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
|
@ -2,9 +2,9 @@ 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";
|
||||||
|
|
||||||
|
import { Button } from "@components/Button";
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
|
||||||
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
|
|
|
@ -2,15 +2,16 @@ import { useEffect } from "react";
|
||||||
|
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { SettingsItem } from "@routes/devices.$id.settings";
|
import { SettingsItem } from "@routes/devices.$id.settings";
|
||||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||||
|
|
||||||
import notifications from "../notifications";
|
import notifications from "../notifications";
|
||||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||||
import { FeatureFlag } from "../components/FeatureFlag";
|
import { FeatureFlag } from "../components/FeatureFlag";
|
||||||
|
|
||||||
|
|
||||||
export default function SettingsHardwareRoute() {
|
export default function SettingsHardwareRoute() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|
||||||
import { Checkbox } from "@/components/Checkbox";
|
import { Checkbox } from "@/components/Checkbox";
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
|
|
||||||
export default function SettingsKeyboardRoute() {
|
export default function SettingsKeyboardRoute() {
|
||||||
const { setKeyboardLayout } = useSettingsStore();
|
const { setKeyboardLayout } = useSettingsStore();
|
||||||
const { showPressedKeys, setShowPressedKeys } = useSettingsStore();
|
const { showPressedKeys, setShowPressedKeys } = useSettingsStore();
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { CheckCircleIcon } from "@heroicons/react/16/solid";
|
import { CheckCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import MouseIcon from "@/assets/mouse-icon.svg";
|
|
||||||
import PointingFinger from "@/assets/pointing-finger.svg";
|
|
||||||
import { GridCard } from "@/components/Card";
|
|
||||||
import { Checkbox } from "@/components/Checkbox";
|
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { JigglerSetting } from "@components/JigglerSetting";
|
import { JigglerSetting } from "@components/JigglerSetting";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
|
import { Checkbox } from "@/components/Checkbox";
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
|
import PointingFinger from "@/assets/pointing-finger.svg";
|
||||||
|
import MouseIcon from "@/assets/mouse-icon.svg";
|
||||||
|
|
||||||
import { cx } from "../cva.config";
|
|
||||||
import notifications from "../notifications";
|
|
||||||
import SettingsNestedSection from "../components/SettingsNestedSection";
|
import SettingsNestedSection from "../components/SettingsNestedSection";
|
||||||
|
import notifications from "../notifications";
|
||||||
|
import { cx } from "../cva.config";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,15 @@ import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { LuEthernetPort } from "react-icons/lu";
|
import { LuEthernetPort } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { GridCard } from "@components/Card";
|
||||||
|
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||||
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
|
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||||
|
import Fieldset from "@/components/Fieldset";
|
||||||
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import {
|
import {
|
||||||
IPv4Mode,
|
IPv4Mode,
|
||||||
IPv6Mode,
|
IPv6Mode,
|
||||||
|
@ -13,20 +22,11 @@ import {
|
||||||
TimeSyncMode,
|
TimeSyncMode,
|
||||||
useNetworkStateStore,
|
useNetworkStateStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { Button } from "@components/Button";
|
|
||||||
import { GridCard } from "@components/Card";
|
|
||||||
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
|
||||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
|
||||||
import Fieldset from "@/components/Fieldset";
|
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
|
|
||||||
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
|
||||||
import EmptyCard from "../components/EmptyCard";
|
|
||||||
import AutoHeight from "../components/AutoHeight";
|
|
||||||
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
||||||
|
import AutoHeight from "../components/AutoHeight";
|
||||||
|
import EmptyCard from "../components/EmptyCard";
|
||||||
|
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useResizeObserver } from "usehooks-ts";
|
import { useResizeObserver } from "usehooks-ts";
|
||||||
|
|
||||||
|
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { LinkButton } from "@/components/Button";
|
import { LinkButton } from "@/components/Button";
|
||||||
import { FeatureFlag } from "@/components/FeatureFlag";
|
import { FeatureFlag } from "@/components/FeatureFlag";
|
||||||
|
@ -23,6 +24,7 @@ import { useUiStore } from "@/hooks/stores";
|
||||||
|
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
|
|
||||||
|
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
export default function SettingsRoute() {
|
export default function SettingsRoute() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
|
import Fieldset from "@components/Fieldset";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { TextAreaWithLabel } from "@/components/TextArea";
|
import { TextAreaWithLabel } from "@/components/TextArea";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
|
||||||
import Fieldset from "@components/Fieldset";
|
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
|
|
||||||
const defaultEdid =
|
const defaultEdid =
|
||||||
"00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b";
|
"00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b";
|
||||||
const edids = [
|
const edids = [
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
|
||||||
|
|
||||||
const loader: LoaderFunction = 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}`, {
|
||||||
|
|
|
@ -15,6 +15,9 @@ import { FocusTrap } from "focus-trap-react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import useWebSocket from "react-use-websocket";
|
import useWebSocket from "react-use-websocket";
|
||||||
|
|
||||||
|
import WebRTCVideo from "@components/WebRTCVideo";
|
||||||
|
import DashboardNavbar from "@components/Header";
|
||||||
|
import { DeviceStatus } from "@routes/welcome-local";
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||||
|
@ -36,11 +39,6 @@ import {
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { useMicrophone } from "@/hooks/useMicrophone";
|
import { useMicrophone } from "@/hooks/useMicrophone";
|
||||||
import { useAudioEvents } from "@/hooks/useAudioEvents";
|
import { useAudioEvents } from "@/hooks/useAudioEvents";
|
||||||
import WebRTCVideo from "@components/WebRTCVideo";
|
|
||||||
import DashboardNavbar from "@components/Header";
|
|
||||||
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats'));
|
|
||||||
const Terminal = lazy(() => import('@components/Terminal'));
|
|
||||||
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import {
|
import {
|
||||||
|
@ -50,10 +48,12 @@ import {
|
||||||
} from "@/components/VideoOverlay";
|
} from "@/components/VideoOverlay";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||||
import { DeviceStatus } from "@routes/welcome-local";
|
|
||||||
import audioQualityService from "@/services/audioQualityService";
|
|
||||||
import { useVersion } from "@/hooks/useVersion";
|
import { useVersion } from "@/hooks/useVersion";
|
||||||
|
|
||||||
|
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats'));
|
||||||
|
const Terminal = lazy(() => import('@components/Terminal'));
|
||||||
|
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
interface LocalLoaderResp {
|
||||||
authMode: "password" | "noPassword" | null;
|
authMode: "password" | "noPassword" | null;
|
||||||
}
|
}
|
||||||
|
@ -573,11 +573,6 @@ export default function KvmIdRoute() {
|
||||||
};
|
};
|
||||||
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
||||||
|
|
||||||
// Register callback with audioQualityService
|
|
||||||
useEffect(() => {
|
|
||||||
audioQualityService.setReconnectionCallback(setupPeerConnection);
|
|
||||||
}, [setupPeerConnection]);
|
|
||||||
|
|
||||||
// TURN server usage detection
|
// TURN server usage detection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (peerConnectionState !== "connected") return;
|
if (peerConnectionState !== "connected") return;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import GridBackground from "@components/GridBackground";
|
||||||
import { LinkButton } from "@/components/Button";
|
import { LinkButton } from "@/components/Button";
|
||||||
import SimpleNavbar from "@/components/SimpleNavbar";
|
import SimpleNavbar from "@/components/SimpleNavbar";
|
||||||
import Container from "@/components/Container";
|
import Container from "@/components/Container";
|
||||||
import GridBackground from "@components/GridBackground";
|
|
||||||
|
|
||||||
export default function DevicesAlreadyAdopted() {
|
export default function DevicesAlreadyAdopted() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -18,6 +18,9 @@ import ExtLink from "../components/ExtLink";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const loader: LoaderFunction = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { useState } from "react";
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
|
||||||
import { DEVICE_API } from "@/ui.config";
|
import { DEVICE_API } from "@/ui.config";
|
||||||
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
|
import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||||
|
|
||||||
import { GridCard } from "../components/Card";
|
import { GridCard } from "../components/Card";
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
|
@ -15,6 +15,7 @@ import api from "../api";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
|
|
||||||
const loader: LoaderFunction = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
|
|
|
@ -16,6 +16,8 @@ import api from "../api";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const loader: LoaderFunction = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { DEVICE_API } from "@/ui.config";
|
||||||
|
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
|
||||||
|
|
||||||
export interface DeviceStatus {
|
export interface DeviceStatus {
|
||||||
isSetup: boolean;
|
isSetup: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import api from '@/api';
|
import { JsonRpcResponse } from '@/hooks/useJsonRpc';
|
||||||
|
|
||||||
interface AudioConfig {
|
interface AudioConfig {
|
||||||
Quality: number;
|
Quality: number;
|
||||||
|
@ -15,6 +15,8 @@ interface AudioQualityResponse {
|
||||||
presets: QualityPresets;
|
presets: QualityPresets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (resp: JsonRpcResponse) => void) => void;
|
||||||
|
|
||||||
class AudioQualityService {
|
class AudioQualityService {
|
||||||
private audioPresets: QualityPresets | null = null;
|
private audioPresets: QualityPresets | null = null;
|
||||||
private microphonePresets: QualityPresets | null = null;
|
private microphonePresets: QualityPresets | null = null;
|
||||||
|
@ -24,25 +26,45 @@ class AudioQualityService {
|
||||||
2: 'High',
|
2: 'High',
|
||||||
3: 'Ultra'
|
3: 'Ultra'
|
||||||
};
|
};
|
||||||
private reconnectionCallback: (() => Promise<void>) | null = null;
|
private rpcSend: RpcSendFunction | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch audio quality presets from the backend
|
* Set RPC send function for cloud compatibility
|
||||||
|
*/
|
||||||
|
setRpcSend(rpcSend: RpcSendFunction): void {
|
||||||
|
this.rpcSend = rpcSend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch audio quality presets using RPC (cloud-compatible)
|
||||||
*/
|
*/
|
||||||
async fetchAudioQualityPresets(): Promise<AudioQualityResponse | null> {
|
async fetchAudioQualityPresets(): Promise<AudioQualityResponse | null> {
|
||||||
|
if (!this.rpcSend) {
|
||||||
|
console.error('RPC not available for audio quality presets');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.GET('/audio/quality');
|
return await new Promise<AudioQualityResponse | null>((resolve) => {
|
||||||
if (response.ok) {
|
this.rpcSend!("audioQualityPresets", {}, (resp: JsonRpcResponse) => {
|
||||||
const data = await response.json();
|
if ("error" in resp) {
|
||||||
|
console.error('RPC audio quality presets failed:', resp.error);
|
||||||
|
resolve(null);
|
||||||
|
} else if ("result" in resp) {
|
||||||
|
const data = resp.result as AudioQualityResponse;
|
||||||
this.audioPresets = data.presets;
|
this.audioPresets = data.presets;
|
||||||
this.updateQualityLabels(data.presets);
|
this.updateQualityLabels(data.presets);
|
||||||
return data;
|
resolve(data);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch audio quality presets:', error);
|
console.error('Failed to fetch audio quality presets:', error);
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update quality labels with actual bitrates from presets
|
* Update quality labels with actual bitrates from presets
|
||||||
|
@ -80,34 +102,25 @@ class AudioQualityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set reconnection callback for WebRTC reset
|
* Set audio quality using RPC (cloud-compatible)
|
||||||
*/
|
|
||||||
setReconnectionCallback(callback: () => Promise<void>): void {
|
|
||||||
this.reconnectionCallback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger audio track replacement using backend's track replacement mechanism
|
|
||||||
*/
|
|
||||||
private async replaceAudioTrack(): Promise<void> {
|
|
||||||
if (this.reconnectionCallback) {
|
|
||||||
await this.reconnectionCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set audio quality with track replacement
|
|
||||||
*/
|
*/
|
||||||
async setAudioQuality(quality: number): Promise<boolean> {
|
async setAudioQuality(quality: number): Promise<boolean> {
|
||||||
try {
|
if (!this.rpcSend) {
|
||||||
const response = await api.POST('/audio/quality', { quality });
|
console.error('RPC not available for audio quality change');
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.replaceAudioTrack();
|
try {
|
||||||
return true;
|
return await new Promise<boolean>((resolve) => {
|
||||||
|
this.rpcSend!("audioQuality", { quality }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
console.error('RPC audio quality change failed:', resp.error);
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set audio quality:', error);
|
console.error('Failed to set audio quality:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
10
web.go
10
web.go
|
@ -184,16 +184,6 @@ func setupRouter() *gin.Engine {
|
||||||
protected.PUT("/auth/password-local", handleUpdatePassword)
|
protected.PUT("/auth/password-local", handleUpdatePassword)
|
||||||
protected.DELETE("/auth/local-password", handleDeletePassword)
|
protected.DELETE("/auth/local-password", handleDeletePassword)
|
||||||
protected.POST("/storage/upload", handleUploadHttp)
|
protected.POST("/storage/upload", handleUploadHttp)
|
||||||
|
|
||||||
// Audio handlers
|
|
||||||
protected.GET("/audio/status", handleAudioStatus)
|
|
||||||
protected.POST("/audio/mute", handleAudioMute)
|
|
||||||
protected.GET("/audio/quality", handleAudioQuality)
|
|
||||||
protected.POST("/audio/quality", handleSetAudioQuality)
|
|
||||||
protected.POST("/microphone/start", handleMicrophoneStart)
|
|
||||||
protected.POST("/microphone/stop", handleMicrophoneStop)
|
|
||||||
protected.POST("/microphone/mute", handleMicrophoneMute)
|
|
||||||
protected.POST("/microphone/reset", handleMicrophoneReset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catch-all route for SPA
|
// Catch-all route for SPA
|
||||||
|
|
Loading…
Reference in New Issue