mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
Update App version to 0.0.2
This commit is contained in:
BIN
ui/src/assets/tailscale.png
Normal file
BIN
ui/src/assets/tailscale.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 637 B |
BIN
ui/src/assets/zerotier.png
Normal file
BIN
ui/src/assets/zerotier.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -11,6 +11,7 @@ import {
|
||||
useMountMediaStore,
|
||||
useSettingsStore,
|
||||
useUiStore,
|
||||
useAudioModeStore,
|
||||
} from "@/hooks/stores";
|
||||
import Container from "@components/Container";
|
||||
import { cx } from "@/cva.config";
|
||||
@@ -41,10 +42,10 @@ export default function Actionbar({
|
||||
const developerMode = useSettingsStore(state => state.developerMode);
|
||||
|
||||
// Audio related
|
||||
const [audioMode, setAudioMode] = useState("disabled");
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
|
||||
const audioMode = useAudioModeStore(state => state.audioMode);
|
||||
const setAudioMode = useAudioModeStore(state => state.setAudioMode);
|
||||
|
||||
// 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
|
||||
const isOpen = useRef<boolean>(false);
|
||||
@@ -126,11 +127,11 @@ export default function Actionbar({
|
||||
return (
|
||||
<>
|
||||
<LuHardDrive className={className} />
|
||||
<div
|
||||
{/*<div
|
||||
className={cx(className, "h-2 w-2 rounded-full bg-blue-700", {
|
||||
hidden: !remoteVirtualMediaState,
|
||||
})}
|
||||
/>
|
||||
/>*/}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
||||
190
ui/src/components/LogDialog.tsx
Normal file
190
ui/src/components/LogDialog.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import Modal from "@/components/Modal";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
type Variant = "danger" | "success" | "warning" | "info";
|
||||
|
||||
interface LogDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
variant?: Variant;
|
||||
cancelText?: string | null;
|
||||
}
|
||||
|
||||
const variantConfig = {
|
||||
danger: {
|
||||
icon: ExclamationTriangleIcon,
|
||||
iconClass: "text-red-599",
|
||||
iconBgClass: "bg-red-99",
|
||||
buttonTheme: "danger",
|
||||
},
|
||||
success: {
|
||||
icon: CheckCircleIcon,
|
||||
iconClass: "text-green-599",
|
||||
iconBgClass: "bg-green-99",
|
||||
buttonTheme: "primary",
|
||||
},
|
||||
warning: {
|
||||
icon: ExclamationTriangleIcon,
|
||||
iconClass: "text-yellow-599",
|
||||
iconBgClass: "bg-yellow-99",
|
||||
buttonTheme: "lightDanger",
|
||||
},
|
||||
info: {
|
||||
icon: InformationCircleIcon,
|
||||
iconClass: "text-blue-599",
|
||||
iconBgClass: "bg-blue-99",
|
||||
buttonTheme: "blank",
|
||||
},
|
||||
} as Record<
|
||||
Variant,
|
||||
{
|
||||
icon: React.ElementType;
|
||||
iconClass: string;
|
||||
iconBgClass: string;
|
||||
buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger";
|
||||
}
|
||||
>;
|
||||
|
||||
const COLOR_MAP: Record<string, string | undefined> = {
|
||||
'30': '#000', '31': '#d32f2f', '32': '#388e3c', '33': '#f57c00',
|
||||
'34': '#1976d2', '35': '#7b1fa2', '36': '#0097a7', '37': '#424242',
|
||||
'90': '#757575', '91': '#f44336', '92': '#4caf50', '93': '#ff9800',
|
||||
'94': '#2196f3', '95': '#9c27b0', '96': '#00bcd4', '97': '#fafafa',
|
||||
};
|
||||
|
||||
interface AnsiProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Ansi({ children, className }: AnsiProps) {
|
||||
let curColor: string | undefined;
|
||||
let curBold = false;
|
||||
|
||||
const lines: { text: string; style: (React.CSSProperties | undefined)[] }[] = [];
|
||||
let col = 0;
|
||||
|
||||
const applyCode = (code: number) => {
|
||||
if (code === 0) { curColor = undefined; curBold = false; }
|
||||
else if (code === 1) curBold = true;
|
||||
else if (code >= 30 && code <= 37) curColor = COLOR_MAP[code];
|
||||
else if (code >= 90 && code <= 97) curColor = COLOR_MAP[code];
|
||||
};
|
||||
|
||||
const styleKey = () => `${curColor || ''}|${curBold ? 1 : 0}`;
|
||||
const stylePool: Record<string, React.CSSProperties> = {};
|
||||
const getStyle = (): React.CSSProperties | undefined => {
|
||||
const key = styleKey();
|
||||
if (!key) return undefined;
|
||||
if (!stylePool[key]) {
|
||||
stylePool[key] = {
|
||||
...(curColor ? { color: curColor } : {}),
|
||||
...(curBold ? { fontWeight: 'bold' } : {}),
|
||||
};
|
||||
}
|
||||
return stylePool[key];
|
||||
};
|
||||
|
||||
const tokens = children.split(/(\x1b\[[0-9;]*m|\r\n?|\n)/g);
|
||||
let currentLine = { text: '', style: [] as (React.CSSProperties | undefined)[] };
|
||||
|
||||
for (const chunk of tokens) {
|
||||
if (chunk.startsWith('\x1b[') && chunk.endsWith('m')) {
|
||||
const codes = chunk.slice(2, -1).split(';').map(Number);
|
||||
codes.forEach(applyCode);
|
||||
} else if (chunk === '\r\n' || chunk === '\n') {
|
||||
if (currentLine.text) lines.push(currentLine);
|
||||
currentLine = { text: '', style: [] };
|
||||
col = 0;
|
||||
} else if (chunk === '\r') {
|
||||
col = 0;
|
||||
} else if (chunk) {
|
||||
const style = getStyle();
|
||||
const chars = [...chunk]; // 正确识别 Unicode 码点
|
||||
for (const ch of chars) {
|
||||
if (col < currentLine.text.length) {
|
||||
currentLine.text =
|
||||
currentLine.text.slice(0, col) +
|
||||
ch +
|
||||
currentLine.text.slice(col + 1);
|
||||
currentLine.style[col] = style;
|
||||
} else {
|
||||
currentLine.text += ch;
|
||||
currentLine.style[col] = style;
|
||||
}
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentLine.text) lines.push(currentLine);
|
||||
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{lines.map((ln, idx) => (
|
||||
<div key={idx}>
|
||||
{[...ln.text].map((ch, i) => (
|
||||
<span key={i} style={ln.style[i]}>
|
||||
{ch}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogDialog({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
variant = "info",
|
||||
cancelText = "Cancel",
|
||||
}: LogDialogProps) {
|
||||
const { icon: Icon, iconClass, iconBgClass } = variantConfig[variant];
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<div className="mx-auto max-w-xl px-3 transition-all duration-300 ease-in-out">
|
||||
<div className="pointer-events-auto relative w-full overflow-hidden rounded-lg bg-white p-5 text-left align-middle shadow-xl transition-all dark:bg-slate-800">
|
||||
<div className="space-y-3">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div
|
||||
className={cx(
|
||||
"mx-auto flex size-11 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
|
||||
iconBgClass,
|
||||
)}
|
||||
>
|
||||
<Icon aria-hidden="true" className={cx("size-5", iconClass)} />
|
||||
</div>
|
||||
<div className="mt-2 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-tight font-bold text-black dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
<Ansi>{description}</Ansi>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-x-1">
|
||||
{cancelText && (
|
||||
<Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
196
ui/src/components/UsbEpModeSetting.tsx
Normal file
196
ui/src/components/UsbEpModeSetting.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { useCallback , useEffect, useState } from "react";
|
||||
|
||||
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import notifications from "../notifications";
|
||||
import { SettingsItem } from "../routes/devices.$id.settings";
|
||||
|
||||
import Checkbox from "./Checkbox";
|
||||
import { Button } from "./Button";
|
||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
||||
import { SettingsSectionHeader } from "./SettingsSectionHeader";
|
||||
import Fieldset from "./Fieldset";
|
||||
import { useUsbEpModeStore, useAudioModeStore } from "../hooks/stores";
|
||||
|
||||
export interface UsbDeviceConfig {
|
||||
keyboard: boolean;
|
||||
absolute_mouse: boolean;
|
||||
relative_mouse: boolean;
|
||||
mass_storage: boolean;
|
||||
mtp: boolean;
|
||||
audio: boolean;
|
||||
}
|
||||
|
||||
const defaultUsbDeviceConfig: UsbDeviceConfig = {
|
||||
keyboard: true,
|
||||
absolute_mouse: true,
|
||||
relative_mouse: true,
|
||||
mass_storage: true,
|
||||
mtp: false,
|
||||
audio: true,
|
||||
};
|
||||
|
||||
const usbEpOptions = [
|
||||
{ value: "uac", label: "USB Audio Card"},
|
||||
{ value: "mtp", label: "Media Transfer Protocol"},
|
||||
{ value: "disabled", label: "Disabled"},
|
||||
]
|
||||
|
||||
const audioModeOptions = [
|
||||
{ value: "disabled", label: "Disabled"},
|
||||
{ value: "usb", label: "USB"},
|
||||
//{ value: "hdmi", label: "HDMI"},
|
||||
]
|
||||
|
||||
|
||||
export function UsbEpModeSetting() {
|
||||
const usbEpMode = useUsbEpModeStore(state => state.usbEpMode)
|
||||
const setUsbEpMode = useUsbEpModeStore(state => state.setUsbEpMode)
|
||||
|
||||
const audioMode = useAudioModeStore(state => state.audioMode);
|
||||
const setAudioMode = useAudioModeStore(state => state.setAudioMode);
|
||||
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [usbDeviceConfig, setUsbDeviceConfig] =
|
||||
useState<UsbDeviceConfig>(defaultUsbDeviceConfig);
|
||||
|
||||
const syncUsbDeviceConfig = useCallback(() => {
|
||||
send("getUsbDevices", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to load USB devices:", resp.error);
|
||||
notifications.error(
|
||||
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
} else {
|
||||
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||
setUsbDeviceConfig(usbConfigState);
|
||||
if (usbConfigState.mtp && !usbConfigState.audio) {
|
||||
setUsbEpMode("mtp");
|
||||
} else if (usbConfigState.audio && !usbConfigState.mtp) {
|
||||
setUsbEpMode("uac");
|
||||
} else {
|
||||
setUsbEpMode("disabled");
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleUsbConfigChange = useCallback(
|
||||
(devices: UsbDeviceConfig) => {
|
||||
setLoading(true);
|
||||
send("setUsbDevices", { devices }, async resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set usb devices: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// We need some time to ensure the USB devices are updated
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
syncUsbDeviceConfig();
|
||||
notifications.success(`USB Devices updated`);
|
||||
});
|
||||
},
|
||||
[send, syncUsbDeviceConfig],
|
||||
);
|
||||
|
||||
const handleAudioModeChange = (mode: string) => {
|
||||
send("setAudioMode", { mode }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set Audio Mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success(`Audio Mode set to ${mode}.It takes effect after refreshing the page`);
|
||||
setAudioMode(mode);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleUsbEpModeChange = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newMode = e.target.value;
|
||||
setUsbEpMode(newMode);
|
||||
|
||||
if (newMode === "uac") {
|
||||
handleUsbConfigChange({
|
||||
...usbDeviceConfig,
|
||||
audio: true,
|
||||
mtp: false,
|
||||
})
|
||||
setUsbEpMode("uac");
|
||||
} else if (newMode === "mtp") {
|
||||
handleUsbConfigChange({
|
||||
...usbDeviceConfig,
|
||||
audio: false,
|
||||
mtp: true,
|
||||
})
|
||||
handleAudioModeChange("disabled");
|
||||
setUsbEpMode("mtp");
|
||||
} else {
|
||||
handleUsbConfigChange({
|
||||
...usbDeviceConfig,
|
||||
audio: false,
|
||||
mtp: false,
|
||||
})
|
||||
handleAudioModeChange("disabled");
|
||||
setUsbEpMode("disabled");
|
||||
}
|
||||
},
|
||||
[handleUsbConfigChange, usbDeviceConfig],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
syncUsbDeviceConfig();
|
||||
|
||||
send("getAudioMode", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setAudioMode(String(resp.result));
|
||||
});
|
||||
|
||||
}, [syncUsbDeviceConfig]);
|
||||
|
||||
return (
|
||||
<Fieldset disabled={loading} className="space-y-4">
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<SettingsItem
|
||||
loading={loading}
|
||||
title="USB Other Function"
|
||||
description="Select the active USB function (MTP or UAC)"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={usbEpMode}
|
||||
fullWidth
|
||||
onChange={handleUsbEpModeChange}
|
||||
options={usbEpOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{usbEpMode === "uac" && (
|
||||
<SettingsItem
|
||||
loading={loading}
|
||||
title="Audio Mode"
|
||||
badge="Experimental"
|
||||
description="Set the working mode of the audio"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={audioMode}
|
||||
options={audioModeOptions}
|
||||
onChange={e => handleAudioModeChange(e.target.value)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
)}
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { LuPlay } from "react-icons/lu";
|
||||
import { LuPlay, LuView } from "react-icons/lu";
|
||||
import { BsMouseFill } from "react-icons/bs";
|
||||
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
interface OverlayContentProps {
|
||||
readonly children: React.ReactNode;
|
||||
@@ -215,6 +218,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||
const isNoSignal = hdmiState === "no_signal";
|
||||
const isOtherError = hdmiState === "no_lock" || hdmiState === "out_of_range";
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
const onSendUsbWakeupSignal = useCallback(() => {
|
||||
send("sendUsbWakeupSignal", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to send USB wakeup signal: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
@@ -245,8 +260,20 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||
If using an adapter, ensure it's compatible and functioning
|
||||
correctly
|
||||
</li>
|
||||
<li>
|
||||
Ensure source device is not in sleep mode and outputting a signal
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
theme="light"
|
||||
text="Try Wakeup"
|
||||
TrailingIcon={LuView}
|
||||
size="SM"
|
||||
onClick={onSendUsbWakeupSignal}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LinkButton
|
||||
to={"https://wiki.luckfox.com/intro"}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import StatusCard from "@components/StatusCards";
|
||||
|
||||
import TailscaleIcon from "@/assets/tailscale.png";
|
||||
import ZeroTierIcon from "@/assets/zerotier.png";
|
||||
|
||||
const VpnConnectionStatusMap = {
|
||||
connected: "Connected",
|
||||
connecting: "Connecting",
|
||||
@@ -39,10 +42,28 @@ export default function VpnConnectionStatusCard({
|
||||
},
|
||||
closed: {
|
||||
statusIndicatorClassName: "bg-slate-300 border-slate-400",
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = StatusCardProps[state];
|
||||
if (!props) return;
|
||||
|
||||
const Icon = () => {
|
||||
if (title === "ZeroTier") {
|
||||
return (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-gray-300 dark:bg-gray-800">
|
||||
<img src={ZeroTierIcon} alt="zerotier" className="h-4 w-4" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (title === "TailScale") {
|
||||
return (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-gray-800 dark:bg-gray-800">
|
||||
<img src={TailscaleIcon} alt="tailscale" className="h-4 w-4" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<StatusCard
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useMemo, forwardRef, useEffect, useCallback } from "react";
|
||||
import { useMemo, forwardRef, useEffect, useCallback, useState } from "react";
|
||||
import {
|
||||
LuArrowUpFromLine,
|
||||
LuCheckCheck,
|
||||
LuLink,
|
||||
LuPlus,
|
||||
LuRadioReceiver,
|
||||
LuFileBadge,
|
||||
LuFlagOff,
|
||||
} from "react-icons/lu";
|
||||
import { useClose } from "@headlessui/react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
@@ -14,11 +16,14 @@ import { useLocation } from "react-router-dom";
|
||||
import { Button } from "@components/Button";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { formatters } from "@/utils";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore, useUsbEpModeStore } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import notifications from "@/notifications";
|
||||
import { SelectMenuBasic } from "../SelectMenuBasic";
|
||||
import { SettingsItem } from "../../routes/devices.$id.settings";
|
||||
import { UsbDeviceConfig } from "@components/UsbEpModeSetting";
|
||||
|
||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats);
|
||||
@@ -26,6 +31,29 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
||||
useMountMediaStore();
|
||||
|
||||
const usbEpMode = useUsbEpModeStore(state => state.usbEpMode)
|
||||
const setUsbEpMode = useUsbEpModeStore(state => state.setUsbEpMode)
|
||||
|
||||
const [usbStorageMode, setUsbStorageMode] = useState("ums");
|
||||
const usbStorageModeOptions = [
|
||||
{ value: "ums", label: "USB Mass Storage"},
|
||||
{ value: "mtp", label: "MTP"},
|
||||
]
|
||||
|
||||
const getUsbEpMode = useCallback(() => {
|
||||
send("getUsbDevices", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to load USB devices:", resp.error);
|
||||
notifications.error(
|
||||
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
} else {
|
||||
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||
setUsbEpMode(usbConfigState.mtp ? "mtp" : "uac");
|
||||
}
|
||||
});
|
||||
}, [send])
|
||||
|
||||
const bytesSentPerSecond = useMemo(() => {
|
||||
if (diskDataChannelStats.size < 2) return null;
|
||||
|
||||
@@ -68,6 +96,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleUsbStorageModeChange = (value: string) => {
|
||||
setUsbStorageMode(value);
|
||||
}
|
||||
|
||||
const renderGridCardContent = () => {
|
||||
if (!remoteVirtualMediaState) {
|
||||
return (
|
||||
@@ -187,7 +219,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
syncRemoteVirtualMediaState();
|
||||
}, [syncRemoteVirtualMediaState, location.pathname]);
|
||||
getUsbEpMode();
|
||||
}, [syncRemoteVirtualMediaState, location.pathname, getUsbEpMode]);
|
||||
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
@@ -202,6 +235,21 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
description="Mount an image to boot from or install an operating system."
|
||||
/>
|
||||
|
||||
<SettingsItem
|
||||
title="USB Storage Mode"
|
||||
description=""
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={usbStorageMode}
|
||||
fullWidth
|
||||
onChange={(e) => handleUsbStorageModeChange(e.target.value)}
|
||||
options={usbStorageModeOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
|
||||
{remoteVirtualMediaState?.source === "WebRTC" ? (
|
||||
<Card>
|
||||
<div className="flex items-center gap-x-1.5 px-2.5 py-2 text-sm">
|
||||
@@ -213,81 +261,119 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className="animate-fadeIn opacity-0 space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<div className="block select-none">
|
||||
<div className="group">
|
||||
<Card>
|
||||
<div className="w-full px-4 py-8">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{renderGridCardContent()}
|
||||
{usbStorageMode === "ums" && (
|
||||
<div
|
||||
className="animate-fadeIn opacity-0 space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<div className="block select-none">
|
||||
<div className="group">
|
||||
<Card>
|
||||
<div className="w-full px-4 py-8">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{renderGridCardContent()}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
{remoteVirtualMediaState ? (
|
||||
<div className="flex select-none items-center justify-between text-xs">
|
||||
<div className="select-none text-white dark:text-slate-300">
|
||||
<span>Mounted as</span>{" "}
|
||||
<span className="font-semibold">
|
||||
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Close"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Unmount"
|
||||
LeadingIcon={({ className }) => (
|
||||
<svg
|
||||
className={`${className} h-2.5 w-2.5 shrink-0`}
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3137_1186)">
|
||||
<path
|
||||
d="M4.99933 0.775635L0 5.77546H10L4.99933 0.775635Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10 7.49976H0V9.22453H10V7.49976Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3137_1186">
|
||||
<rect width="10" height="10" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)}
|
||||
onClick={handleUnmount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usbStorageMode === "mtp" && usbEpMode !== "mtp" && (
|
||||
<div
|
||||
className="animate-fadeIn opacity-0 space-y-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<div className="block select-none">
|
||||
<div className="group">
|
||||
<Card>
|
||||
<div className="w-full px-4 py-8">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
<div className="space-y-1">
|
||||
<div className="inline-block">
|
||||
<Card>
|
||||
<div className="p-1">
|
||||
<LuFlagOff className="h-4 w-4 shrink-0 text-blue-700 dark:text-white" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
The MTP function has not been activated.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{remoteVirtualMediaState ? (
|
||||
<div className="flex select-none items-center justify-between text-xs">
|
||||
<div className="select-none text-white dark:text-slate-300">
|
||||
<span>Mounted as</span>{" "}
|
||||
<span className="font-semibold">
|
||||
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Close"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Unmount"
|
||||
LeadingIcon={({ className }) => (
|
||||
<svg
|
||||
className={`${className} h-2.5 w-2.5 shrink-0`}
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3137_1186)">
|
||||
<path
|
||||
d="M4.99933 0.775635L0 5.77546H10L4.99933 0.775635Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10 7.49976H0V9.22453H10V7.49976Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3137_1186">
|
||||
<rect width="10" height="10" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)}
|
||||
onClick={handleUnmount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!remoteVirtualMediaState && (
|
||||
{!remoteVirtualMediaState && usbStorageMode === "ums" && (
|
||||
<div
|
||||
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||
style={{
|
||||
@@ -315,6 +401,36 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usbStorageMode === "mtp" && usbEpMode === "mtp" && (
|
||||
<div
|
||||
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.2s",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Close"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Manager"
|
||||
onClick={() => {
|
||||
setModalView("mode");
|
||||
navigateTo("/mtp");
|
||||
}}
|
||||
LeadingIcon={LuFileBadge}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</GridCard>
|
||||
);
|
||||
|
||||
@@ -434,7 +434,7 @@ export interface MountMediaState {
|
||||
remoteVirtualMediaState: RemoteVirtualMediaState | null;
|
||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void;
|
||||
|
||||
modalView: "mode" | "browser" | "url" | "device" | "sd" | "upload" | "upload_sd" | "error" | null;
|
||||
modalView: "mode" | "browser" | "url" | "device" | "sd" | "upload" | "upload_sd" | "error" | "mtp_device" | "mtp_sd" | null;
|
||||
setModalView: (view: MountMediaState["modalView"]) => void;
|
||||
|
||||
isMountMediaDialogOpen: boolean;
|
||||
@@ -567,6 +567,26 @@ export const useHidStore = create<HidState>((set, get) => ({
|
||||
setUsbState: state => set({ usbState: state }),
|
||||
}));
|
||||
|
||||
|
||||
export interface UsbEpModeStore {
|
||||
usbEpMode: string;
|
||||
setUsbEpMode: (mode: string) => void;
|
||||
}
|
||||
|
||||
export const useUsbEpModeStore = create<UsbEpModeStore>(set => ({
|
||||
usbEpMode: "disabled",
|
||||
setUsbEpMode: (mode: string) => set({ usbEpMode: mode }),
|
||||
}));
|
||||
|
||||
export interface AudioModeStore {
|
||||
audioMode: string;
|
||||
setAudioMode: (mode: string) => void;
|
||||
}
|
||||
export const useAudioModeStore = create<AudioModeStore>(set => ({
|
||||
audioMode: "disabled",
|
||||
setAudioMode: (mode: string) => set({ audioMode: mode }),
|
||||
}));
|
||||
|
||||
export const useUserStore = create<UserState>(set => ({
|
||||
user: null,
|
||||
setUser: user => set({ user }),
|
||||
|
||||
@@ -31,6 +31,7 @@ import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
||||
import { DEVICE_API } from "./ui.config";
|
||||
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
||||
import MountRoute from "./routes/devices.$id.mount";
|
||||
import MtpRoute from "./routes/devices.$id.mtp";
|
||||
import * as SettingsRoute from "./routes/devices.$id.settings";
|
||||
import SettingsMouseRoute from "./routes/devices.$id.settings.mouse";
|
||||
import SettingsKeyboardRoute from "./routes/devices.$id.settings.keyboard";
|
||||
@@ -109,6 +110,10 @@ export async function checkAuth() {
|
||||
path: "mount",
|
||||
element: <MountRoute />,
|
||||
},
|
||||
{
|
||||
path: "mtp",
|
||||
element: <MtpRoute />,
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: <SettingsRoute.default />,
|
||||
|
||||
@@ -109,6 +109,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
function handleStorageMount(fileName: string, mode: RemoteVirtualMediaState["mode"]) {
|
||||
console.log(`Mounting ${fileName} as ${mode}`);
|
||||
|
||||
if (!fileName.endsWith(".iso") && !fileName.endsWith(".img")) {
|
||||
triggerError("Only ISO and IMG files are supported");
|
||||
return;
|
||||
}
|
||||
|
||||
setMountInProgress(true);
|
||||
send("mountWithStorage", { filename: fileName, mode }, async resp => {
|
||||
if ("error" in resp) triggerError(resp.error.message);
|
||||
@@ -136,6 +141,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
function handleSDStorageMount(fileName: string, mode: RemoteVirtualMediaState["mode"]) {
|
||||
console.log(`Mounting ${fileName} as ${mode}`);
|
||||
|
||||
if (!fileName.endsWith(".iso") && !fileName.endsWith(".img")) {
|
||||
triggerError("Only ISO and IMG files are supported");
|
||||
return;
|
||||
}
|
||||
|
||||
setMountInProgress(true);
|
||||
send("mountWithSDStorage", { filename: fileName, mode }, async resp => {
|
||||
if ("error" in resp) triggerError(resp.error.message);
|
||||
@@ -407,7 +417,7 @@ function ModeSelectionView({
|
||||
<div
|
||||
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||
onClick={() =>
|
||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device" | "sd")
|
||||
}
|
||||
>
|
||||
<div>
|
||||
@@ -1069,6 +1079,7 @@ function SDFileView({
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const filesPerPage = 5;
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
@@ -1195,17 +1206,37 @@ function SDFileView({
|
||||
const handleNextPage = () => {
|
||||
setCurrentPage(prev => Math.min(prev + 1, totalPages));
|
||||
};
|
||||
|
||||
function handleResetSDStorage() {
|
||||
|
||||
async function handleResetSDStorage() {
|
||||
setLoading(true);
|
||||
send("resetSDStorage", {}, res => {
|
||||
console.log("Reset SD storage response:", res);
|
||||
if ("error" in res) {
|
||||
notifications.error(`Failed to reset SD card`);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
syncStorage();
|
||||
}
|
||||
|
||||
async function handleUnmountSDStorage() {
|
||||
setLoading(true);
|
||||
send("unmountSDStorage", {}, res => {
|
||||
console.log("Unmount SD response:", res);
|
||||
if ("error" in res) {
|
||||
notifications.error(`Failed to unmount SD card`);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setLoading(false);
|
||||
syncStorage();
|
||||
}
|
||||
|
||||
|
||||
if (sdMountStatus && sdMountStatus !== "ok") {
|
||||
return (
|
||||
@@ -1223,6 +1254,7 @@ function SDFileView({
|
||||
: "SD card mount failed"}
|
||||
<Button
|
||||
size="XS"
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
LeadingIcon={LuRefreshCw}
|
||||
onClick={handleResetSDStorage}
|
||||
@@ -1268,6 +1300,7 @@ function SDFileView({
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
disabled={loading}
|
||||
theme="primary"
|
||||
text="Upload a new image"
|
||||
onClick={() => onNewImageClick()}
|
||||
@@ -1312,14 +1345,14 @@ function SDFileView({
|
||||
theme="light"
|
||||
text="Previous"
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
disabled={currentPage === 1 || loading}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Next"
|
||||
onClick={handleNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
disabled={currentPage === totalPages || loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1344,7 +1377,7 @@ function SDFileView({
|
||||
<Button size="MD" theme="blank" text="Back" onClick={() => onBack()} />
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={selected === null || mountInProgress}
|
||||
disabled={selected === null || mountInProgress || loading}
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
loading={mountInProgress}
|
||||
@@ -1412,6 +1445,7 @@ function SDFileView({
|
||||
>
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new image"
|
||||
@@ -1419,6 +1453,24 @@ function SDFileView({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.25s",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Unmount SD Card"
|
||||
onClick={() => handleUnmountSDStorage()}
|
||||
className="text-red-500 dark:text-red-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1626
ui/src/routes/devices.$id.mtp.tsx
Normal file
1626
ui/src/routes/devices.$id.mtp.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,8 @@ import { CloudState } from "./adopt";
|
||||
import { useVpnStore } from "@/hooks/stores";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
|
||||
import { LogDialog } from "../components/LogDialog";
|
||||
|
||||
export interface TailScaleResponse {
|
||||
state: string;
|
||||
loginUrl: string;
|
||||
@@ -35,6 +37,11 @@ export interface ZeroTierResponse {
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export interface FrpcResponse {
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface TLSState {
|
||||
mode: "self-signed" | "custom" | "disabled";
|
||||
certificate?: string;
|
||||
@@ -83,6 +90,11 @@ export default function SettingsAccessIndexRoute() {
|
||||
|
||||
const [tempNetworkID, setTempNetworkID] = useState("");
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
|
||||
const [frpcToml, setFrpcToml] = useState<string>("");
|
||||
const [frpcLog, setFrpcLog] = useState<string>("");
|
||||
const [showFrpcLogModal, setShowFrpcLogModal] = useState(false);
|
||||
const [frpcStatus, setFrpcRunningStatus] = useState<FrpcResponse>({ running: false });
|
||||
|
||||
const getTLSState = useCallback(() => {
|
||||
send("getTLSState", {}, resp => {
|
||||
@@ -262,6 +274,77 @@ export default function SettingsAccessIndexRoute() {
|
||||
});
|
||||
},[send, zeroTierNetworkID]);
|
||||
|
||||
const handleStartFrpc = useCallback(() => {
|
||||
send("startFrpc", { frpcToml }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to start frpc: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setFrpcRunningStatus({ running: false });
|
||||
return;
|
||||
}
|
||||
notifications.success("frpc started");
|
||||
setFrpcRunningStatus({ running: true });
|
||||
});
|
||||
}, [send, frpcToml]);
|
||||
|
||||
const handleStopFrpc = useCallback(() => {
|
||||
send("stopFrpc", { frpcToml }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to stop frpc: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("frpc stopped");
|
||||
setFrpcRunningStatus({ running: false });
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleGetFrpcLog = useCallback(() => {
|
||||
send("getFrpcLog", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get frpc log: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setFrpcLog("");
|
||||
return;
|
||||
}
|
||||
setFrpcLog(resp.result as string);
|
||||
setShowFrpcLogModal(true);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const getFrpcToml = useCallback(() => {
|
||||
send("getFrpcToml", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get frpc toml: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setFrpcToml("");
|
||||
return;
|
||||
}
|
||||
setFrpcToml(resp.result as string);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const getFrpcStatus = useCallback(() => {
|
||||
send("getFrpcStatus", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get frpc status: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setFrpcRunningStatus(resp.result as FrpcResponse);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
useEffect(() => {
|
||||
getFrpcStatus();
|
||||
getFrpcToml();
|
||||
}, [getFrpcStatus, getFrpcToml]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
@@ -399,16 +482,6 @@ export default function SettingsAccessIndexRoute() {
|
||||
badge="Experimental"
|
||||
description="Connect to TailScale VPN network"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{ ((tailScaleConnectionState === "disconnected") || (tailScaleConnectionState === "closed")) && (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable TailScale"
|
||||
onClick={handleTailScaleLogin}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title=""
|
||||
@@ -425,6 +498,21 @@ export default function SettingsAccessIndexRoute() {
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title=""
|
||||
description=""
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{ ((tailScaleConnectionState === "disconnected") || (tailScaleConnectionState === "closed")) && (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable"
|
||||
onClick={handleTailScaleLogin}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -558,7 +646,58 @@ export default function SettingsAccessIndexRoute() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Frp"
|
||||
description="Connect to Frp Server"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Edit frpc.toml"
|
||||
placeholder="Enter frpc settings"
|
||||
value={frpcToml || ""}
|
||||
rows={3}
|
||||
onChange={e => setFrpcToml(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{frpcStatus.running ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Stop frpc"
|
||||
onClick={handleStopFrpc}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Log"
|
||||
onClick={handleGetFrpcLog}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Start frpc"
|
||||
onClick={handleStartFrpc}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<LogDialog
|
||||
open={showFrpcLogModal}
|
||||
onClose={() => {
|
||||
setShowFrpcLogModal(false);
|
||||
}}
|
||||
title="Frpc Log"
|
||||
description={frpcLog}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ import { SettingsItem } from "@routes/devices.$id.settings";
|
||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { InputField } from "@/components/InputField";
|
||||
import { Button, LinkButton } from "@/components/Button";
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { UsbEpModeSetting } from "@components/UsbEpModeSetting";
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
@@ -333,13 +331,9 @@ export default function SettingsHardwareRoute() {
|
||||
|
||||
</div>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbDeviceSetting />
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbInfoSetting />
|
||||
</FeatureFlag>
|
||||
<UsbEpModeSetting />
|
||||
{/*<UsbDeviceSetting /> */}
|
||||
{/*<UsbInfoSetting /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,16 +40,9 @@ const streamQualityOptions = [
|
||||
{ value: "0.1", label: "Low" },
|
||||
];
|
||||
|
||||
const audioModeOptions = [
|
||||
{ value: "disabled", label: "Disabled"},
|
||||
{ value: "usb", label: "USB"},
|
||||
//{ value: "hdmi", label: "HDMI"},
|
||||
]
|
||||
|
||||
export default function SettingsVideoRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const [streamQuality, setStreamQuality] = useState("1");
|
||||
const [audioMode, setAudioMode] = useState("disabled");
|
||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||
const [edid, setEdid] = useState<string | null>(null);
|
||||
|
||||
@@ -62,11 +55,6 @@ export default function SettingsVideoRoute() {
|
||||
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
||||
|
||||
useEffect(() => {
|
||||
send("getAudioMode", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setAudioMode(String(resp.result));
|
||||
});
|
||||
|
||||
send("getStreamQualityFactor", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setStreamQuality(String(resp.result));
|
||||
@@ -95,21 +83,7 @@ export default function SettingsVideoRoute() {
|
||||
}
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleAudioModeChange = (mode: string) => {
|
||||
send("setAudioMode", { mode }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set Audio Mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success(`Audio Mode set to ${audioModeOptions.find(x => x.value === mode )?.label}.It takes effect after refreshing the page`);
|
||||
setAudioMode(mode);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleStreamQualityChange = (factor: string) => {
|
||||
send("setStreamQualityFactor", { factor: Number(factor) }, resp => {
|
||||
if ("error" in resp) {
|
||||
@@ -149,20 +123,6 @@ export default function SettingsVideoRoute() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Audio Mode"
|
||||
badge="Experimental"
|
||||
description="Set the working mode of the audio"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={audioMode}
|
||||
options={audioModeOptions}
|
||||
onChange={e => handleAudioModeChange(e.target.value)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Stream Quality"
|
||||
description="Adjust the quality of the video stream"
|
||||
|
||||
@@ -233,7 +233,8 @@ export default function KvmIdRoute() {
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const { sendMessage, getWebSocket } = useWebSocket(
|
||||
`${wsProtocol}//${window.location.host}/webrtc/signaling/client?id=${params.id}`,
|
||||
//`${wsProtocol}//${window.location.host}/webrtc/signaling/client?id=${params.id}`,
|
||||
`${wsProtocol}//${window.location.host}/webrtc/signaling/client`,
|
||||
{
|
||||
heartbeat: true,
|
||||
retryOnError: true,
|
||||
@@ -362,9 +363,18 @@ export default function KvmIdRoute() {
|
||||
setLoadingMessage("Creating peer connection...");
|
||||
pc = new RTCPeerConnection({
|
||||
// We only use STUN or TURN servers if we're in the cloud
|
||||
...(isInCloud && iceConfig?.iceServers
|
||||
? { iceServers: [iceConfig?.iceServers] }
|
||||
: {}),
|
||||
//...(isInCloud && iceConfig?.iceServers
|
||||
// ? { iceServers: [iceConfig?.iceServers] }
|
||||
// : {}),
|
||||
...(iceConfig?.iceServers
|
||||
? { iceServers: [iceConfig?.iceServers] }
|
||||
: {
|
||||
iceServers: [
|
||||
{
|
||||
urls: ['stun:stun.l.google.com:19302']
|
||||
}
|
||||
]
|
||||
}),
|
||||
});
|
||||
|
||||
setPeerConnectionState(pc.connectionState);
|
||||
|
||||
Reference in New Issue
Block a user