mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
cceb8d18ed
...
ce5fd0fe14
Author | SHA1 | Date |
---|---|---|
|
ce5fd0fe14 | |
|
69168ff062 | |
|
a819739790 | |
|
7602aefe98 |
|
@ -10,6 +10,7 @@
|
||||||
"dev": "vite dev --mode=development",
|
"dev": "vite dev --mode=development",
|
||||||
"build": "npm run build:prod",
|
"build": "npm run build:prod",
|
||||||
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
||||||
|
"dev:device": "vite dev --mode=device",
|
||||||
"build:prod": "tsc && vite build --mode=production",
|
"build:prod": "tsc && vite build --mode=production",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,18 +4,21 @@ import {
|
||||||
useMountMediaStore,
|
useMountMediaStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
|
useVideoStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { MdOutlineContentPasteGo } from "react-icons/md";
|
import { MdOutlineContentPasteGo } from "react-icons/md";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import PasteModal from "@/components/popovers/PasteModal";
|
import PasteModal from "@/components/popovers/PasteModal";
|
||||||
import { FaKeyboard } from "react-icons/fa6";
|
import { FaKeyboard, FaLock } from "react-icons/fa6";
|
||||||
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import MountPopopover from "./popovers/MountPopover";
|
import MountPopopover from "./popovers/MountPopover";
|
||||||
import { Fragment, useCallback, useRef } from "react";
|
import { Fragment, useCallback, useRef } from "react";
|
||||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export default function Actionbar({
|
export default function Actionbar({
|
||||||
requestFullscreen,
|
requestFullscreen,
|
||||||
|
@ -33,6 +36,7 @@ export default function Actionbar({
|
||||||
state => state.remoteVirtualMediaState,
|
state => state.remoteVirtualMediaState,
|
||||||
);
|
);
|
||||||
const developerMode = useSettingsStore(state => state.developerMode);
|
const developerMode = useSettingsStore(state => state.developerMode);
|
||||||
|
const hdmiState = useVideoStore(state => state.hdmiState);
|
||||||
|
|
||||||
// This is the only way to get a reliable state change for the popover
|
// This is the only way to get a reliable state change for the popover
|
||||||
// at time of writing this there is no mount, or unmount event for the popover
|
// at time of writing this there is no mount, or unmount event for the popover
|
||||||
|
@ -52,6 +56,8 @@ export default function Actionbar({
|
||||||
[setDisableFocusTrap],
|
[setDisableFocusTrap],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
|
<Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
|
||||||
<div
|
<div
|
||||||
|
@ -203,6 +209,23 @@ export default function Actionbar({
|
||||||
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{useSettingsStore().actionBarCtrlAltDel && (
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Ctrl + Alt + Del"
|
||||||
|
LeadingIcon={FaLock}
|
||||||
|
onClick={() => {
|
||||||
|
sendKeyboardEvent(
|
||||||
|
[keys["Delete"]],
|
||||||
|
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
||||||
|
);
|
||||||
|
setTimeout(resetKeyboardState, 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||||
|
@ -247,6 +270,7 @@ export default function Actionbar({
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Fullscreen"
|
text="Fullscreen"
|
||||||
|
disabled={hdmiState !== 'ready'}
|
||||||
LeadingIcon={LuMaximize}
|
LeadingIcon={LuMaximize}
|
||||||
onClick={() => requestFullscreen()}
|
onClick={() => requestFullscreen()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -30,6 +30,8 @@ export default function WebRTCVideo() {
|
||||||
const {
|
const {
|
||||||
setClientSize: setVideoClientSize,
|
setClientSize: setVideoClientSize,
|
||||||
setSize: setVideoSize,
|
setSize: setVideoSize,
|
||||||
|
width: videoWidth,
|
||||||
|
height: videoHeight,
|
||||||
clientWidth: videoClientWidth,
|
clientWidth: videoClientWidth,
|
||||||
clientHeight: videoClientHeight,
|
clientHeight: videoClientHeight,
|
||||||
} = useVideoStore();
|
} = useVideoStore();
|
||||||
|
@ -102,20 +104,43 @@ export default function WebRTCVideo() {
|
||||||
const mouseMoveHandler = useCallback(
|
const mouseMoveHandler = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
if (!videoClientWidth || !videoClientHeight) return;
|
if (!videoClientWidth || !videoClientHeight) return;
|
||||||
const { buttons } = e;
|
// Get the aspect ratios of the video element and the video stream
|
||||||
|
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
|
||||||
|
const videoStreamAspectRatio = videoWidth / videoHeight;
|
||||||
|
|
||||||
// Clamp mouse position within the video boundaries
|
// Calculate the effective video display area
|
||||||
const currMouseX = Math.min(Math.max(1, e.offsetX), videoClientWidth);
|
let effectiveWidth = videoClientWidth;
|
||||||
const currMouseY = Math.min(Math.max(1, e.offsetY), videoClientHeight);
|
let effectiveHeight = videoClientHeight;
|
||||||
|
let offsetX = 0;
|
||||||
|
let offsetY = 0;
|
||||||
|
|
||||||
// Normalize mouse position to 0-32767 range (HID absolute coordinate system)
|
if (videoElementAspectRatio > videoStreamAspectRatio) {
|
||||||
const x = Math.round((currMouseX / videoClientWidth) * 32767);
|
// Pillarboxing: black bars on the left and right
|
||||||
const y = Math.round((currMouseY / videoClientHeight) * 32767);
|
effectiveWidth = videoClientHeight * videoStreamAspectRatio;
|
||||||
|
offsetX = (videoClientWidth - effectiveWidth) / 2;
|
||||||
|
} else if (videoElementAspectRatio < videoStreamAspectRatio) {
|
||||||
|
// Letterboxing: black bars on the top and bottom
|
||||||
|
effectiveHeight = videoClientWidth / videoStreamAspectRatio;
|
||||||
|
offsetY = (videoClientHeight - effectiveHeight) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp mouse position within the effective video boundaries
|
||||||
|
const clampedX = Math.min(Math.max(offsetX, e.offsetX), offsetX + effectiveWidth);
|
||||||
|
const clampedY = Math.min(Math.max(offsetY, e.offsetY), offsetY + effectiveHeight);
|
||||||
|
|
||||||
|
// Map clamped mouse position to the video stream's coordinate system
|
||||||
|
const relativeX = (clampedX - offsetX) / effectiveWidth;
|
||||||
|
const relativeY = (clampedY - offsetY) / effectiveHeight;
|
||||||
|
|
||||||
|
// Convert to HID absolute coordinate system (0-32767 range)
|
||||||
|
const x = Math.round(relativeX * 32767);
|
||||||
|
const y = Math.round(relativeY * 32767);
|
||||||
|
|
||||||
// Send mouse movement
|
// Send mouse movement
|
||||||
|
const { buttons } = e;
|
||||||
sendMouseMovement(x, y, buttons);
|
sendMouseMovement(x, y, buttons);
|
||||||
},
|
},
|
||||||
[sendMouseMovement, videoClientHeight, videoClientWidth],
|
[sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseWheelHandler = useCallback(
|
const mouseWheelHandler = useCallback(
|
||||||
|
|
|
@ -796,6 +796,15 @@ export default function SettingsSidebar() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<SettingsItem
|
||||||
|
title="Ctrl + Alt + Del Button"
|
||||||
|
description="Display Ctrl + Alt + Del button on the Action Bar"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.actionBarCtrlAltDel}
|
||||||
|
onChange={e => settings.setActionBarCtrlAltDel(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
|
|
|
@ -270,6 +270,9 @@ interface SettingsState {
|
||||||
// Add new developer mode state
|
// Add new developer mode state
|
||||||
developerMode: boolean;
|
developerMode: boolean;
|
||||||
setDeveloperMode: (enabled: boolean) => void;
|
setDeveloperMode: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: boolean;
|
||||||
|
setActionBarCtrlAltDel: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -287,6 +290,9 @@ export const useSettingsStore = create(
|
||||||
// Add developer mode with default value
|
// Add developer mode with default value
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: false,
|
||||||
|
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
|
|
@ -2,13 +2,31 @@ import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
declare const process: {
|
||||||
|
env: {
|
||||||
|
JETKVM_PROXY_URL: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig(({ mode, command }) => {
|
||||||
const isCloud = mode === "production";
|
const isCloud = mode === "production";
|
||||||
const onDevice = mode === "device";
|
const onDevice = mode === "device";
|
||||||
|
const { JETKVM_PROXY_URL } = process.env;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [tsconfigPaths(), react()],
|
plugins: [tsconfigPaths(), react()],
|
||||||
build: { outDir: isCloud ? "dist" : "../static" },
|
build: { outDir: isCloud ? "dist" : "../static" },
|
||||||
server: { host: "0.0.0.0" },
|
server: {
|
||||||
base: onDevice ? "/static" : "/",
|
host: "0.0.0.0",
|
||||||
|
proxy: JETKVM_PROXY_URL ? {
|
||||||
|
'/me': JETKVM_PROXY_URL,
|
||||||
|
'/device': JETKVM_PROXY_URL,
|
||||||
|
'/webrtc': JETKVM_PROXY_URL,
|
||||||
|
'/auth': JETKVM_PROXY_URL,
|
||||||
|
'/storage': JETKVM_PROXY_URL,
|
||||||
|
'/cloud': JETKVM_PROXY_URL,
|
||||||
|
} : undefined
|
||||||
|
},
|
||||||
|
base: onDevice && command === 'build' ? "/static" : "/",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue