mirror of https://github.com/jetkvm/kvm.git
Merge cedad5f516
into d79f359c43
This commit is contained in:
commit
0deef7bd8a
File diff suppressed because it is too large
Load Diff
|
@ -19,8 +19,8 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
||||
"@xterm/addon-clipboard": "^0.1.0",
|
||||
|
@ -36,44 +36,43 @@
|
|||
"framer-motion": "^11.15.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react": "^19.1.0",
|
||||
"react-animate-height": "^3.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-simple-keyboard": "^3.7.112",
|
||||
"react-simple-keyboard": "^3.8.71",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.9",
|
||||
"recharts": "^2.15.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^2.15.3",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"validator": "^13.12.0",
|
||||
"xterm": "^5.3.0",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"validator": "^13.15.0",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/validator": "^13.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||
"@typescript-eslint/parser": "^8.25.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/react": "^19.1.3",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/validator": "^13.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
||||
"@typescript-eslint/parser": "^8.32.0",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^5.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { JSX } from "react";
|
||||
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
||||
|
||||
import ExtLink from "@/components/ExtLink";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Ref } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Ref } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
|
|
@ -79,10 +79,11 @@ function Terminal({
|
|||
return () => {
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
};
|
||||
}, [enableTerminal, instance, ref, setDisableKeyboardFocusTrap, type]);
|
||||
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
|
||||
|
||||
const readyState = dataChannel.readyState;
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
if (readyState !== "open") return;
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
@ -93,11 +94,10 @@ function Terminal({
|
|||
// Handle binary data differently based on browser implementation
|
||||
// Firefox sends data as blobs, chrome sends data as arraybuffer
|
||||
if (binaryType === "arraybuffer") {
|
||||
instance?.write(new Uint8Array(e.data));
|
||||
instance.write(new Uint8Array(e.data));
|
||||
} else if (binaryType === "blob") {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (!instance) return;
|
||||
if (!reader.result) return;
|
||||
instance.write(new Uint8Array(reader.result as ArrayBuffer));
|
||||
};
|
||||
|
@ -107,12 +107,12 @@ function Terminal({
|
|||
{ signal: abortController.signal },
|
||||
);
|
||||
|
||||
const onDataHandler = instance?.onData(data => {
|
||||
const onDataHandler = instance.onData(data => {
|
||||
dataChannel.send(data);
|
||||
});
|
||||
|
||||
// Setup escape key handler
|
||||
const onKeyHandler = instance?.onKey(e => {
|
||||
const onKeyHandler = instance.onKey(e => {
|
||||
const { domEvent } = e;
|
||||
if (domEvent.key === "Escape") {
|
||||
setTerminalType("none");
|
||||
|
@ -123,32 +123,32 @@ function Terminal({
|
|||
|
||||
// Send initial terminal size
|
||||
if (dataChannel.readyState === "open") {
|
||||
dataChannel.send(JSON.stringify({ rows: instance?.rows, cols: instance?.cols }));
|
||||
dataChannel.send(JSON.stringify({ rows: instance.rows, cols: instance.cols }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
onDataHandler?.dispose();
|
||||
onKeyHandler?.dispose();
|
||||
onDataHandler.dispose();
|
||||
onKeyHandler.dispose();
|
||||
};
|
||||
}, [dataChannel, instance, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
|
||||
// Load the fit addon
|
||||
const fitAddon = new FitAddon();
|
||||
instance?.loadAddon(fitAddon);
|
||||
instance.loadAddon(fitAddon);
|
||||
|
||||
instance?.loadAddon(new ClipboardAddon());
|
||||
instance?.loadAddon(new Unicode11Addon());
|
||||
instance?.loadAddon(new WebLinksAddon());
|
||||
instance.loadAddon(new ClipboardAddon());
|
||||
instance.loadAddon(new Unicode11Addon());
|
||||
instance.loadAddon(new WebLinksAddon());
|
||||
instance.unicode.activeVersion = "11";
|
||||
|
||||
if (isWebGl2Supported) {
|
||||
const webGl2Addon = new WebglAddon();
|
||||
webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
|
||||
instance?.loadAddon(webGl2Addon);
|
||||
instance.loadAddon(webGl2Addon);
|
||||
}
|
||||
|
||||
const handleResize = () => fitAddon.fit();
|
||||
|
@ -158,13 +158,11 @@ function Terminal({
|
|||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [ref, instance, dataChannel]);
|
||||
}, [ref, instance]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
onKeyUp={e => e.stopPropagation()}
|
||||
>
|
||||
<div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
useVideoStore,
|
||||
} from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { useResizeObserver } from "@/hooks/useResizeObserver";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
import { cx } from "@/cva.config";
|
||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||
import Actionbar from "@components/ActionBar";
|
||||
|
@ -67,7 +67,7 @@ export default function WebRTCVideo() {
|
|||
|
||||
// Video-related
|
||||
useResizeObserver({
|
||||
ref: videoElm,
|
||||
ref: videoElm as React.RefObject<HTMLElement>,
|
||||
onResize: ({ width, height }) => {
|
||||
// This is actually client size, not videoSize
|
||||
if (width && height) {
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function useInterval(callback: () => void, delay: number) {
|
||||
const savedCallback = useRef<typeof callback>();
|
||||
|
||||
// Save the callback directly in the useRef object
|
||||
savedCallback.current = callback;
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
if (!savedCallback.current) return;
|
||||
savedCallback.current();
|
||||
}
|
||||
|
||||
if (delay !== null) {
|
||||
const id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Custom hook that determines if the component is currently mounted.
|
||||
* @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted.
|
||||
* @public
|
||||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted)
|
||||
* @example
|
||||
* ```tsx
|
||||
* const isComponentMounted = useIsMounted();
|
||||
* // Use isComponentMounted() to check if the component is currently mounted before performing certain actions.
|
||||
* ```
|
||||
*/
|
||||
export function useIsMounted(): () => boolean {
|
||||
const isMounted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useCallback(() => isMounted.current, []);
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import type { RefObject } from "react";
|
||||
|
||||
import { useIsMounted } from "./useIsMounted";
|
||||
|
||||
/** The size of the observed element. */
|
||||
interface Size {
|
||||
/** The width of the observed element. */
|
||||
width: number | undefined;
|
||||
/** The height of the observed element. */
|
||||
height: number | undefined;
|
||||
}
|
||||
|
||||
/** The options for the ResizeObserver. */
|
||||
interface UseResizeObserverOptions<T extends HTMLElement = HTMLElement> {
|
||||
/** The ref of the element to observe. */
|
||||
ref: RefObject<T>;
|
||||
/**
|
||||
* When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.
|
||||
* @default undefined
|
||||
*/
|
||||
onResize?: (size: Size) => void;
|
||||
/**
|
||||
* The box model to use for the ResizeObserver.
|
||||
* @default 'content-box'
|
||||
*/
|
||||
box?: "border-box" | "content-box" | "device-pixel-content-box";
|
||||
}
|
||||
|
||||
const initialSize: Size = {
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook that observes the size of an element using the ResizeObserver API.
|
||||
* @template T - The type of the element to observe.
|
||||
* @param {UseResizeObserverOptions<T>} options - The options for the ResizeObserver.
|
||||
* @returns {Size} - The size of the observed element.
|
||||
* @public
|
||||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-resize-observer)
|
||||
* @see [MDN ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
||||
* @example
|
||||
* ```tsx
|
||||
* const myRef = useRef(null);
|
||||
* const { width = 0, height = 0 } = useResizeObserver({
|
||||
* ref: myRef,
|
||||
* box: 'content-box',
|
||||
* });
|
||||
*
|
||||
* <div ref={myRef}>Hello, world!</div>
|
||||
* ```
|
||||
*/
|
||||
export function useResizeObserver<T extends HTMLElement = HTMLElement>(
|
||||
options: UseResizeObserverOptions<T>,
|
||||
): Size {
|
||||
const { ref, box = "content-box" } = options;
|
||||
const [{ width, height }, setSize] = useState<Size>(initialSize);
|
||||
const isMounted = useIsMounted();
|
||||
const previousSize = useRef<Size>({ ...initialSize });
|
||||
const onResize = useRef<((size: Size) => void) | undefined>(undefined);
|
||||
onResize.current = options.onResize;
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
if (typeof window === "undefined" || !("ResizeObserver" in window)) return;
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
const boxProp =
|
||||
box === "border-box"
|
||||
? "borderBoxSize"
|
||||
: box === "device-pixel-content-box"
|
||||
? "devicePixelContentBoxSize"
|
||||
: "contentBoxSize";
|
||||
|
||||
const newWidth = extractSize(entry, boxProp, "inlineSize");
|
||||
const newHeight = extractSize(entry, boxProp, "blockSize");
|
||||
|
||||
const hasChanged =
|
||||
previousSize.current.width !== newWidth ||
|
||||
previousSize.current.height !== newHeight;
|
||||
|
||||
if (hasChanged) {
|
||||
const newSize: Size = { width: newWidth, height: newHeight };
|
||||
previousSize.current.width = newWidth;
|
||||
previousSize.current.height = newHeight;
|
||||
|
||||
if (onResize.current) {
|
||||
onResize.current(newSize);
|
||||
} else {
|
||||
if (isMounted()) {
|
||||
setSize(newSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(ref.current, { box });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [box, isMounted, ref]);
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
/** @private */
|
||||
type BoxSizesKey = keyof Pick<
|
||||
ResizeObserverEntry,
|
||||
"borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize"
|
||||
>;
|
||||
|
||||
function extractSize(
|
||||
entry: ResizeObserverEntry,
|
||||
box: BoxSizesKey,
|
||||
sizeType: keyof ResizeObserverSize,
|
||||
): number | undefined {
|
||||
if (!entry[box]) {
|
||||
if (box === "contentBoxSize") {
|
||||
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Array.isArray(entry[box])
|
||||
? entry[box][0][sizeType]
|
||||
: // @ts-expect-error Support Firefox's non-standard behavior
|
||||
(entry[box][sizeType] as number);
|
||||
}
|
|
@ -19,7 +19,7 @@ import { LinkButton } from "../components/Button";
|
|||
import { cx } from "../cva.config";
|
||||
import { useUiStore } from "../hooks/stores";
|
||||
import useKeyboard from "../hooks/useKeyboard";
|
||||
import { useResizeObserver } from "../hooks/useResizeObserver";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
import LoadingSpinner from "../components/LoadingSpinner";
|
||||
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
|
@ -30,7 +30,7 @@ export default function SettingsRoute() {
|
|||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
||||
const [showRightGradient, setShowRightGradient] = useState(false);
|
||||
const { width } = useResizeObserver({ ref: scrollContainerRef });
|
||||
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
|
||||
|
||||
// Handle scroll position to show/hide gradients
|
||||
const handleScroll = () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
|||
import DashboardNavbar from "@components/Header";
|
||||
import { LinkButton } from "@components/Button";
|
||||
import KvmCard from "@components/KvmCard";
|
||||
import useInterval from "@/hooks/useInterval";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
import { checkAuth } from "@/main";
|
||||
import { User } from "@/hooks/stores";
|
||||
import EmptyCard from "@components/EmptyCard";
|
||||
|
|
Loading…
Reference in New Issue