mirror of https://github.com/jetkvm/kvm.git
Compare commits
6 Commits
afce9afb4d
...
2f2f45ddf0
| Author | SHA1 | Date |
|---|---|---|
|
|
2f2f45ddf0 | |
|
|
9e95cc3f8a | |
|
|
31ea366e51 | |
|
|
b9705f4bac | |
|
|
7f2dcc84b4 | |
|
|
bc1992ea13 |
6
Makefile
6
Makefile
|
|
@ -205,12 +205,14 @@ lint-go-fix: build_audio_deps
|
||||||
# Run UI linting locally (mirrors GitHub workflow ui-lint.yml)
|
# Run UI linting locally (mirrors GitHub workflow ui-lint.yml)
|
||||||
lint-ui:
|
lint-ui:
|
||||||
@echo "Running UI lint..."
|
@echo "Running UI lint..."
|
||||||
@cd ui && npm ci && npm run lint
|
@cd ui && npm ci
|
||||||
|
@cd ui && npm run lint
|
||||||
|
|
||||||
# Run UI linting with auto-fix
|
# Run UI linting with auto-fix
|
||||||
lint-ui-fix:
|
lint-ui-fix:
|
||||||
@echo "Running UI lint with auto-fix..."
|
@echo "Running UI lint with auto-fix..."
|
||||||
@cd ui && npm ci && npm run lint:fix
|
@cd ui && npm ci
|
||||||
|
@cd ui && npm run lint:fix
|
||||||
|
|
||||||
# Legacy alias for UI linting (for backward compatibility)
|
# Legacy alias for UI linting (for backward compatibility)
|
||||||
ui-lint: lint-ui
|
ui-lint: lint-ui
|
||||||
|
|
|
||||||
109
audio.go
109
audio.go
|
|
@ -14,7 +14,7 @@ import (
|
||||||
var (
|
var (
|
||||||
audioMutex sync.Mutex
|
audioMutex sync.Mutex
|
||||||
outputSource audio.AudioSource
|
outputSource audio.AudioSource
|
||||||
inputSource atomic.Pointer[audio.AudioSource]
|
inputSource audio.AudioSource
|
||||||
outputRelay *audio.OutputRelay
|
outputRelay *audio.OutputRelay
|
||||||
inputRelay *audio.InputRelay
|
inputRelay *audio.InputRelay
|
||||||
audioInitialized bool
|
audioInitialized bool
|
||||||
|
|
@ -63,15 +63,13 @@ func startAudio() error {
|
||||||
|
|
||||||
// Start input audio if not running, USB audio enabled, and input enabled
|
// Start input audio if not running, USB audio enabled, and input enabled
|
||||||
ensureConfigLoaded()
|
ensureConfigLoaded()
|
||||||
if inputSource.Load() == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio {
|
if inputSource == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio {
|
||||||
alsaPlaybackDevice := "hw:1,0" // USB speakers
|
alsaPlaybackDevice := "hw:1,0" // USB speakers
|
||||||
|
|
||||||
// Create CGO audio source
|
// Create CGO audio source
|
||||||
newInputSource := audio.NewCgoInputSource(alsaPlaybackDevice)
|
inputSource = audio.NewCgoInputSource(alsaPlaybackDevice)
|
||||||
var audioSrc audio.AudioSource = newInputSource
|
|
||||||
inputSource.Store(&audioSrc)
|
|
||||||
|
|
||||||
inputRelay = audio.NewInputRelay(newInputSource)
|
inputRelay = audio.NewInputRelay(inputSource)
|
||||||
if err := inputRelay.Start(); err != nil {
|
if err := inputRelay.Start(); err != nil {
|
||||||
audioLogger.Error().Err(err).Msg("Failed to start input relay")
|
audioLogger.Error().Err(err).Msg("Failed to start input relay")
|
||||||
}
|
}
|
||||||
|
|
@ -80,41 +78,31 @@ func startAudio() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopOutputLocked stops output audio (assumes mutex is held)
|
|
||||||
func stopOutputLocked() {
|
|
||||||
if outputRelay != nil {
|
|
||||||
outputRelay.Stop()
|
|
||||||
outputRelay = nil
|
|
||||||
}
|
|
||||||
if outputSource != nil {
|
|
||||||
outputSource.Disconnect()
|
|
||||||
outputSource = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopInputLocked stops input audio (assumes mutex is held)
|
|
||||||
func stopInputLocked() {
|
|
||||||
if inputRelay != nil {
|
|
||||||
inputRelay.Stop()
|
|
||||||
inputRelay = nil
|
|
||||||
}
|
|
||||||
if source := inputSource.Load(); source != nil {
|
|
||||||
(*source).Disconnect()
|
|
||||||
inputSource.Store(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopAudioLocked stops all audio (assumes mutex is held)
|
|
||||||
func stopAudioLocked() {
|
|
||||||
stopOutputLocked()
|
|
||||||
stopInputLocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopAudio stops all audio
|
|
||||||
func stopAudio() {
|
func stopAudio() {
|
||||||
audioMutex.Lock()
|
audioMutex.Lock()
|
||||||
defer audioMutex.Unlock()
|
outRelay := outputRelay
|
||||||
stopAudioLocked()
|
outSource := outputSource
|
||||||
|
inRelay := inputRelay
|
||||||
|
inSource := inputSource
|
||||||
|
outputRelay = nil
|
||||||
|
outputSource = nil
|
||||||
|
inputRelay = nil
|
||||||
|
inputSource = nil
|
||||||
|
audioMutex.Unlock()
|
||||||
|
|
||||||
|
// Disconnect outside mutex to avoid blocking new sessions during CGO calls
|
||||||
|
if outRelay != nil {
|
||||||
|
outRelay.Stop()
|
||||||
|
}
|
||||||
|
if outSource != nil {
|
||||||
|
outSource.Disconnect()
|
||||||
|
}
|
||||||
|
if inRelay != nil {
|
||||||
|
inRelay.Stop()
|
||||||
|
}
|
||||||
|
if inSource != nil {
|
||||||
|
inSource.Disconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func onWebRTCConnect() {
|
func onWebRTCConnect() {
|
||||||
|
|
@ -171,8 +159,18 @@ func SetAudioOutputEnabled(enabled bool) error {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
audioMutex.Lock()
|
audioMutex.Lock()
|
||||||
defer audioMutex.Unlock()
|
outRelay := outputRelay
|
||||||
stopOutputLocked()
|
outSource := outputSource
|
||||||
|
outputRelay = nil
|
||||||
|
outputSource = nil
|
||||||
|
audioMutex.Unlock()
|
||||||
|
|
||||||
|
if outRelay != nil {
|
||||||
|
outRelay.Stop()
|
||||||
|
}
|
||||||
|
if outSource != nil {
|
||||||
|
outSource.Disconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -190,8 +188,18 @@ func SetAudioInputEnabled(enabled bool) error {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
audioMutex.Lock()
|
audioMutex.Lock()
|
||||||
defer audioMutex.Unlock()
|
inRelay := inputRelay
|
||||||
stopInputLocked()
|
inSource := inputSource
|
||||||
|
inputRelay = nil
|
||||||
|
inputSource = nil
|
||||||
|
audioMutex.Unlock()
|
||||||
|
|
||||||
|
if inRelay != nil {
|
||||||
|
inRelay.Stop()
|
||||||
|
}
|
||||||
|
if inSource != nil {
|
||||||
|
inSource.Disconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -240,22 +248,23 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
|
||||||
continue // Drop frame but keep reading
|
continue // Drop frame but keep reading
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get source atomically (hot path optimization)
|
// Get source in single mutex operation (hot path optimization)
|
||||||
source := inputSource.Load()
|
audioMutex.Lock()
|
||||||
|
source := inputSource
|
||||||
|
audioMutex.Unlock()
|
||||||
|
|
||||||
if source == nil {
|
if source == nil {
|
||||||
continue // No relay, drop frame but keep reading
|
continue // No relay, drop frame but keep reading
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(*source).IsConnected() {
|
if !source.IsConnected() {
|
||||||
if err := (*source).Connect(); err != nil {
|
if err := source.Connect(); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := (*source).WriteMessage(0, opusData); err != nil {
|
if err := source.WriteMessage(0, opusData); err != nil {
|
||||||
(*source).Disconnect()
|
source.Disconnect()
|
||||||
audioLogger.Warn().Err(err).Str("track_id", myTrackID).Msg("failed to write audio message")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
jsonrpc.go
13
jsonrpc.go
|
|
@ -894,11 +894,20 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) {
|
||||||
func updateUsbRelatedConfig(wasAudioEnabled bool) error {
|
func updateUsbRelatedConfig(wasAudioEnabled bool) error {
|
||||||
ensureConfigLoaded()
|
ensureConfigLoaded()
|
||||||
|
|
||||||
// Stop input audio before USB reconfiguration (input uses USB)
|
|
||||||
audioMutex.Lock()
|
audioMutex.Lock()
|
||||||
stopInputLocked()
|
inRelay := inputRelay
|
||||||
|
inSource := inputSource
|
||||||
|
inputRelay = nil
|
||||||
|
inputSource = nil
|
||||||
audioMutex.Unlock()
|
audioMutex.Unlock()
|
||||||
|
|
||||||
|
if inRelay != nil {
|
||||||
|
inRelay.Stop()
|
||||||
|
}
|
||||||
|
if inSource != nil {
|
||||||
|
inSource.Disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
if err := gadget.UpdateGadgetConfig(); err != nil {
|
if err := gadget.UpdateGadgetConfig(); err != nil {
|
||||||
return fmt.Errorf("failed to write gadget config: %w", err)
|
return fmt.Errorf("failed to write gadget config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -133,7 +133,7 @@ func Main() {
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigs
|
<-sigs
|
||||||
|
|
||||||
logger.Log().Msg("JetKVM Shutting Down")
|
logger.Info().Msg("JetKVM Shutting Down")
|
||||||
|
|
||||||
stopAudio()
|
stopAudio()
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2025.10.24.2140",
|
"version": "2025.11.07.2130",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.20.0"
|
"node": "^22.20.0"
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.4",
|
"cva": "^1.0.0-beta.4",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.19",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^11.0.4",
|
"focus-trap-react": "^11.0.4",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
|
|
@ -47,52 +47,52 @@
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
"react-simple-keyboard": "^3.8.131",
|
"react-simple-keyboard": "^3.8.132",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.20",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.1",
|
"@eslint/compat": "^1.4.1",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.39.0",
|
"@eslint/js": "^9.39.1",
|
||||||
"@inlang/cli": "^3.0.12",
|
"@inlang/cli": "^3.0.12",
|
||||||
"@inlang/paraglide-js": "^2.4.0",
|
"@inlang/paraglide-js": "^2.4.0",
|
||||||
"@inlang/plugin-m-function-matcher": "^2.1.0",
|
"@inlang/plugin-m-function-matcher": "^2.1.0",
|
||||||
"@inlang/plugin-message-format": "^4.0.0",
|
"@inlang/plugin-message-format": "^4.0.0",
|
||||||
"@inlang/sdk": "^2.4.9",
|
"@inlang/sdk": "^2.4.9",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/validator": "^13.15.3",
|
"@types/validator": "^13.15.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||||
"@typescript-eslint/parser": "^8.46.2",
|
"@typescript-eslint/parser": "^8.46.3",
|
||||||
"@vitejs/plugin-react-swc": "^4.2.0",
|
"@vitejs/plugin-react-swc": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.5.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.17",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.12",
|
"vite": "^7.1.12",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,9 @@ export interface UpdateState {
|
||||||
|
|
||||||
updateErrorMessage: string | null;
|
updateErrorMessage: string | null;
|
||||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||||
|
|
||||||
|
shouldReload: boolean;
|
||||||
|
setShouldReload: (reloadRequired: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateState>(set => ({
|
export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
|
|
@ -665,6 +668,9 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
updateErrorMessage: null,
|
updateErrorMessage: null,
|
||||||
setUpdateErrorMessage: (errorMessage: string) =>
|
setUpdateErrorMessage: (errorMessage: string) =>
|
||||||
set({ updateErrorMessage: errorMessage }),
|
set({ updateErrorMessage: errorMessage }),
|
||||||
|
|
||||||
|
shouldReload: false,
|
||||||
|
setShouldReload: (reloadRequired: boolean) => set({ shouldReload: reloadRequired }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess";
|
export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess";
|
||||||
|
|
@ -875,12 +881,12 @@ export interface MacrosState {
|
||||||
loadMacros: () => Promise<void>;
|
loadMacros: () => Promise<void>;
|
||||||
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
||||||
sendFn:
|
sendFn:
|
||||||
| ((
|
| ((
|
||||||
method: string,
|
method: string,
|
||||||
params: unknown,
|
params: unknown,
|
||||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||||
) => void)
|
) => void)
|
||||||
| null;
|
| null;
|
||||||
setSendFn: (
|
setSendFn: (
|
||||||
sendFn: (
|
sendFn: (
|
||||||
method: string,
|
method: string,
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,19 @@ import notifications from "../notifications";
|
||||||
|
|
||||||
export default function SettingsAudioRoute() {
|
export default function SettingsAudioRoute() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const settings = useSettingsStore();
|
const { setAudioOutputEnabled, setAudioInputAutoEnable, audioOutputEnabled, audioInputAutoEnable } = useSettingsStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
settings.setAudioOutputEnabled(resp.result as boolean);
|
setAudioOutputEnabled(resp.result as boolean);
|
||||||
});
|
});
|
||||||
|
|
||||||
send("getAudioInputAutoEnable", {}, (resp: JsonRpcResponse) => {
|
send("getAudioInputAutoEnable", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
settings.setAudioInputAutoEnable(resp.result as boolean);
|
setAudioInputAutoEnable(resp.result as boolean);
|
||||||
});
|
});
|
||||||
}, [send, settings]);
|
}, [send, setAudioOutputEnabled, setAudioInputAutoEnable]);
|
||||||
|
|
||||||
const handleAudioOutputEnabledChange = (enabled: boolean) => {
|
const handleAudioOutputEnabledChange = (enabled: boolean) => {
|
||||||
send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => {
|
send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => {
|
||||||
|
|
@ -35,7 +35,7 @@ export default function SettingsAudioRoute() {
|
||||||
notifications.error(errorMsg);
|
notifications.error(errorMsg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.setAudioOutputEnabled(enabled);
|
setAudioOutputEnabled(enabled);
|
||||||
const successMsg = enabled ? m.audio_output_enabled() : m.audio_output_disabled();
|
const successMsg = enabled ? m.audio_output_enabled() : m.audio_output_disabled();
|
||||||
notifications.success(successMsg);
|
notifications.success(successMsg);
|
||||||
});
|
});
|
||||||
|
|
@ -47,7 +47,7 @@ export default function SettingsAudioRoute() {
|
||||||
notifications.error(String(resp.error.data || m.unknown_error()));
|
notifications.error(String(resp.error.data || m.unknown_error()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.setAudioInputAutoEnable(enabled);
|
setAudioInputAutoEnable(enabled);
|
||||||
const successMsg = enabled
|
const successMsg = enabled
|
||||||
? m.audio_input_auto_enable_enabled()
|
? m.audio_input_auto_enable_enabled()
|
||||||
: m.audio_input_auto_enable_disabled();
|
: m.audio_input_auto_enable_disabled();
|
||||||
|
|
@ -67,7 +67,7 @@ export default function SettingsAudioRoute() {
|
||||||
description={m.audio_settings_output_description()}
|
description={m.audio_settings_output_description()}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.audioOutputEnabled || false}
|
checked={audioOutputEnabled || false}
|
||||||
onChange={(e) => handleAudioOutputEnabledChange(e.target.checked)}
|
onChange={(e) => handleAudioOutputEnabledChange(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
@ -77,7 +77,7 @@ export default function SettingsAudioRoute() {
|
||||||
description={m.audio_settings_auto_enable_microphone_description()}
|
description={m.audio_settings_auto_enable_microphone_description()}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.audioInputAutoEnable || false}
|
checked={audioInputAutoEnable || false}
|
||||||
onChange={(e) => handleAudioInputAutoEnableChange(e.target.checked)}
|
onChange={(e) => handleAudioInputAutoEnableChange(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,24 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { updateSuccess } = location.state || {};
|
const { updateSuccess } = location.state || {};
|
||||||
|
|
||||||
const { setModalView, otaState } = useUpdateStore();
|
const { setModalView, otaState, shouldReload, setShouldReload } = useUpdateStore();
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const onClose = useCallback(async () => {
|
const onClose = useCallback(async () => {
|
||||||
navigate(".."); // back to the devices.$id.settings page
|
navigate(".."); // back to the devices.$id.settings page
|
||||||
// Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation.
|
|
||||||
await sleep(1000);
|
if (shouldReload) {
|
||||||
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
|
setShouldReload(false);
|
||||||
}, [navigate]);
|
await sleep(1000); // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation.
|
||||||
|
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
|
||||||
|
}
|
||||||
|
}, [navigate, setShouldReload, shouldReload]);
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
|
setShouldReload(true);
|
||||||
send("tryUpdate", {});
|
send("tryUpdate", {});
|
||||||
setModalView("updating");
|
setModalView("updating");
|
||||||
}, [send, setModalView]);
|
}, [send, setModalView, setShouldReload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (otaState.updating) {
|
if (otaState.updating) {
|
||||||
|
|
@ -133,6 +137,7 @@ function LoadingState({
|
||||||
const { setModalView } = useUpdateStore();
|
const { setModalView } = useUpdateStore();
|
||||||
|
|
||||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
abortControllerRef.current = new AbortController();
|
abortControllerRef.current = new AbortController();
|
||||||
const signal = abortControllerRef.current.signal;
|
const signal = abortControllerRef.current.signal;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue