mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
Update App version to 0.0.3
This commit is contained in:
@@ -38,7 +38,9 @@ import {
|
||||
useRTCStore,
|
||||
} from "../hooks/stores";
|
||||
import { UploadDialog } from "@/components/UploadDialog";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
import { useReactAt } from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function MountRoute() {
|
||||
const navigate = useNavigate();
|
||||
@@ -349,16 +351,17 @@ function ModeSelectionView({
|
||||
selectedMode: "browser" | "url" | "device" | "sd";
|
||||
setSelectedMode: (mode: "browser" | "url" | "device" | "sd") => void;
|
||||
}) {
|
||||
const { $at }= useReactAt();
|
||||
const { setModalView } = useMountMediaStore();
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="animate-fadeIn space-y-0 opacity-0">
|
||||
<h2 className="text-lg leading-tight font-bold dark:text-white">
|
||||
Virtual Media Source
|
||||
{$at("Virtual Media Source")}
|
||||
</h2>
|
||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
Choose how you want to mount your virtual media
|
||||
{$at("Choose how you want to mount your virtual media")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@@ -382,7 +385,7 @@ function ModeSelectionView({
|
||||
{
|
||||
label: "KVM Storage Mount",
|
||||
value: "device",
|
||||
description: "Mount previously uploaded files from the KVM storage",
|
||||
description: "",
|
||||
icon: LuRadioReceiver,
|
||||
tag: null,
|
||||
disabled: false,
|
||||
@@ -390,7 +393,7 @@ function ModeSelectionView({
|
||||
{
|
||||
label: "KVM MicroSD Mount",
|
||||
value: "sd",
|
||||
description: "Mount previously uploaded files from the KVM MicroSD",
|
||||
description: "",
|
||||
icon: LuRadioReceiver,
|
||||
tag: null,
|
||||
disabled: false,
|
||||
@@ -458,14 +461,14 @@ function ModeSelectionView({
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<Button size="MD" theme="blank" onClick={onClose} text="Cancel" />
|
||||
<Button size="MD" theme="blank" onClick={onClose} text={$at("Cancel")} />
|
||||
<Button
|
||||
size="MD"
|
||||
theme="primary"
|
||||
onClick={() => {
|
||||
setModalView(selectedMode);
|
||||
}}
|
||||
text="Continue"
|
||||
text={$at("Continue")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -482,6 +485,7 @@ function BrowserFileView({
|
||||
onMountFile: (file: File, mode: RemoteVirtualMediaState["mode"]) => void;
|
||||
mountInProgress: boolean;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
|
||||
@@ -571,11 +575,11 @@ function BrowserFileView({
|
||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
||||
</Fieldset>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={onBack} />
|
||||
<Button size="MD" theme="blank" text={$at("Back")} onClick={onBack} />
|
||||
<Button
|
||||
size="MD"
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
text={$at("Mount")}
|
||||
onClick={handleMount}
|
||||
disabled={!selectedFile || mountInProgress}
|
||||
loading={mountInProgress}
|
||||
@@ -758,6 +762,7 @@ function DeviceFileView({
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const { $at } = useReactAt();
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -867,13 +872,56 @@ function DeviceFileView({
|
||||
const handleNextPage = () => {
|
||||
setCurrentPage(prev => Math.min(prev + 1, totalPages));
|
||||
};
|
||||
|
||||
const [autoMountSystemInfo, setAutoMountSystemInfo] = useState(false);
|
||||
const handleAutoMountSystemInfoChange = (value: boolean) => {
|
||||
send("setAutoMountSystemInfo", { enabled: value }, response => {
|
||||
if ("error" in response) {
|
||||
notifications.error(`Failed to set auto mount system_info.img: ${response.error.message}`);
|
||||
return;
|
||||
}
|
||||
setAutoMountSystemInfo(value);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
send("getAutoMountSystemInfo", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to load auto mount system_info.img: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setAutoMountSystemInfo(false);
|
||||
} else {
|
||||
setAutoMountSystemInfo(resp.result as boolean);
|
||||
}
|
||||
});
|
||||
}, [send, setAutoMountSystemInfo])
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from KVM Storage"
|
||||
description="Select an image to mount from the KVM storage"
|
||||
title={$at("Mount from KVM Storage")}
|
||||
description={$at("Select the image you want to mount from the KVM storage")}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
animationDuration: "0.7s",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<SettingsItem
|
||||
title={$at("Automatically mount system_info.img")}
|
||||
description={$at("Mount system_info.img automatically when the KVM startup")}
|
||||
>
|
||||
<Checkbox
|
||||
checked={autoMountSystemInfo}
|
||||
onChange={(e) => handleAutoMountSystemInfoChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<hr className="border-slate-800/20 dark:border-slate-300/20" />
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
style={{
|
||||
@@ -888,17 +936,17 @@ function DeviceFileView({
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
No images available
|
||||
{$at("No images available")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Upload an image to start virtual media mounting.
|
||||
{$at("Upload an image to start virtual media mounting.")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Upload a new image"
|
||||
text={$at("Upload a new image")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -919,7 +967,7 @@ function DeviceFileView({
|
||||
if (!selectedFile) return;
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete " + selectedFile.name + "?",
|
||||
$at("Are you sure you want to delete " + selectedFile.name + "?"),
|
||||
)
|
||||
) {
|
||||
handleDeleteFile(selectedFile);
|
||||
@@ -943,14 +991,14 @@ function DeviceFileView({
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Previous"
|
||||
text={$at("Previous")}
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Next"
|
||||
text={$at("Next")}
|
||||
onClick={handleNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
/>
|
||||
@@ -974,12 +1022,12 @@ function DeviceFileView({
|
||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
||||
</Fieldset>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="blank" text={$at("Back")} onClick={() => onBack()} />
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={selected === null || mountInProgress}
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
text={$at("Mount")}
|
||||
loading={mountInProgress}
|
||||
onClick={() =>
|
||||
onMountStorageFile(
|
||||
@@ -999,7 +1047,7 @@ function DeviceFileView({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="MD" theme="light" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="light" text={$at("Back")} onClick={() => onBack()} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1013,10 +1061,10 @@ function DeviceFileView({
|
||||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Available Storage
|
||||
{$at("Available space")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{percentageUsed}% used
|
||||
{percentageUsed}% {$at("used")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700">
|
||||
@@ -1027,10 +1075,10 @@ function DeviceFileView({
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesUsed)} used
|
||||
{formatters.bytes(bytesUsed)} {$at("used")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesFree)} free
|
||||
{formatters.bytes(bytesFree)} {$at("free")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1047,7 +1095,7 @@ function DeviceFileView({
|
||||
size="MD"
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new image"
|
||||
text={$at("Upload a new image")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -1074,7 +1122,7 @@ function SDFileView({
|
||||
createdAt: string;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const { $at }= useReactAt();
|
||||
const [sdMountStatus, setSDMountStatus] = useState<"ok" | "none" | "fail" | null>(null);
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
@@ -1242,16 +1290,16 @@ function SDFileView({
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from KVM MicroSD Card"
|
||||
description="Select an image to mount from the KVM storage"
|
||||
title={$at("Mount from KVM MicroSD Card")}
|
||||
description={$at("Select an image to mount from the KVM storage")}
|
||||
/>
|
||||
<div className="flex items-center justify-center py-8 text-center">
|
||||
<div className="space-y-3">
|
||||
<ExclamationTriangleIcon className="mx-auto h-6 w-6 text-red-500" />
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
{sdMountStatus === "none"
|
||||
? "No SD card detected"
|
||||
: "SD card mount failed"}
|
||||
? $at("No SD card detected")
|
||||
: $at("SD card mount failed")}
|
||||
<Button
|
||||
size="XS"
|
||||
disabled={loading}
|
||||
@@ -1262,8 +1310,8 @@ function SDFileView({
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{sdMountStatus === "none"
|
||||
? "Please insert an SD card and try again."
|
||||
: "Please format the SD card and try again."}
|
||||
? $at("Please insert an SD card and try again.")
|
||||
: $at("Please format the SD card and try again.")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1274,8 +1322,8 @@ function SDFileView({
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from KVM MicroSD Card"
|
||||
description="Select an image to mount from the KVM storage"
|
||||
title={$at("Mount from KVM MicroSD Card")}
|
||||
description={$at("Select an image to mount from the KVM storage")}
|
||||
/>
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
@@ -1291,10 +1339,10 @@ function SDFileView({
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
No images available
|
||||
{$at("No images available")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Upload an image to start virtual media mounting.
|
||||
{$at("Upload an image to start virtual media mounting.")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1302,7 +1350,7 @@ function SDFileView({
|
||||
size="SM"
|
||||
disabled={loading}
|
||||
theme="primary"
|
||||
text="Upload a new image"
|
||||
text={$at("Upload a new image")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -1321,7 +1369,7 @@ function SDFileView({
|
||||
onDelete={() => {
|
||||
const selectedFile = onStorageFiles.find(f => f.name === file.name);
|
||||
if (!selectedFile) return;
|
||||
if (window.confirm("Are you sure you want to delete " + selectedFile.name + "?")) {
|
||||
if (window.confirm($at("Are you sure you want to delete " + selectedFile.name + "?") )) {
|
||||
handleSDDeleteFile(selectedFile);
|
||||
}
|
||||
}}
|
||||
@@ -1343,14 +1391,14 @@ function SDFileView({
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Previous"
|
||||
text={$at("Previous")}
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1 || loading}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Next"
|
||||
text={$at("Next")}
|
||||
onClick={handleNextPage}
|
||||
disabled={currentPage === totalPages || loading}
|
||||
/>
|
||||
@@ -1374,12 +1422,12 @@ function SDFileView({
|
||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
||||
</Fieldset>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="blank" text={$at("Back")} onClick={() => onBack()} />
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={selected === null || mountInProgress || loading}
|
||||
theme="primary"
|
||||
text="Mount File"
|
||||
text={$at("Mount File")}
|
||||
loading={mountInProgress}
|
||||
onClick={() =>
|
||||
onMountStorageFile(
|
||||
@@ -1399,7 +1447,7 @@ function SDFileView({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="MD" theme="light" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="light" text={$at("Back")} onClick={() => onBack()} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1413,10 +1461,10 @@ function SDFileView({
|
||||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Available Storage
|
||||
{$at("Available Space")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{percentageUsed}% used
|
||||
{percentageUsed}% {$at("used")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-sm bg-slate-200 dark:bg-slate-700">
|
||||
@@ -1427,10 +1475,10 @@ function SDFileView({
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesUsed)} used
|
||||
{formatters.bytes(bytesUsed)} {$at("used")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesFree)} free
|
||||
{formatters.bytes(bytesFree)} {$at("free")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1448,7 +1496,7 @@ function SDFileView({
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new image"
|
||||
text={$at("Upload a New Image")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -1466,7 +1514,7 @@ function SDFileView({
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Unmount SD Card"
|
||||
text={$at("Unmount Micro SD Card")}
|
||||
onClick={() => handleUnmountSDStorage()}
|
||||
className="text-red-500 dark:text-red-400"
|
||||
/>
|
||||
@@ -1489,6 +1537,7 @@ function UploadFileView({
|
||||
const [uploadState, setUploadState] = useState<"idle" | "uploading" | "success">(
|
||||
"idle",
|
||||
);
|
||||
const { $at }= useReactAt();
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadedFileName, setUploadedFileName] = useState<string | null>(null);
|
||||
const [uploadedFileSize, setUploadedFileSize] = useState<number | null>(null);
|
||||
@@ -1767,16 +1816,16 @@ function UploadFileView({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<UploadDialog
|
||||
open={true}
|
||||
title="Upload New Image"
|
||||
title={$at("Upload a New Image")}
|
||||
description={
|
||||
incompleteFileName
|
||||
? `Continue uploading "${incompleteFileName}"`
|
||||
: "Select an image file to upload to KVM storage"
|
||||
? $at(`Continue uploading "${incompleteFileName}"`)
|
||||
: $at("Select an image file to upload to KVM storage")
|
||||
}
|
||||
>
|
||||
<div
|
||||
@@ -1813,11 +1862,11 @@ function UploadFileView({
|
||||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
{incompleteFileName
|
||||
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
|
||||
: "Click to select a file"}
|
||||
? `"${$at("Click to select")}" "${incompleteFileName.replace(".incomplete", "")}"`
|
||||
: $at("Click to select a file")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Supported formats: ISO, IMG
|
||||
{$at("Supported formats: ISO, IMG")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1832,7 +1881,7 @@ function UploadFileView({
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="leading-non text-lg font-semibold text-black dark:text-white">
|
||||
Uploading {formatters.truncateMiddle(uploadedFileName, 30)}
|
||||
{$at("Uploading")} {formatters.truncateMiddle(uploadedFileName, 30)}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(uploadedFileSize || 0)}
|
||||
@@ -1845,11 +1894,11 @@ function UploadFileView({
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-slate-600 dark:text-slate-400">
|
||||
<span>Uploading...</span>
|
||||
<span>{$at("Uploading...")}</span>
|
||||
<span>
|
||||
{uploadSpeed !== null
|
||||
? `${formatters.bytes(uploadSpeed)}/s`
|
||||
: "Calculating..."}
|
||||
: $at("Calculating...")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1866,11 +1915,10 @@ function UploadFileView({
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
Upload successful
|
||||
{$at("Upload Successful")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.truncateMiddle(uploadedFileName, 40)} has been
|
||||
uploaded
|
||||
{formatters.truncateMiddle(uploadedFileName, 40)} {$at("Uploaded")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1913,7 +1961,7 @@ function UploadFileView({
|
||||
<Button
|
||||
size="MD"
|
||||
theme="light"
|
||||
text="Cancel Upload"
|
||||
text={$at("Cancel Upload")}
|
||||
onClick={() => {
|
||||
onCancelUpload();
|
||||
setUploadState("idle");
|
||||
@@ -1927,7 +1975,7 @@ function UploadFileView({
|
||||
<Button
|
||||
size="MD"
|
||||
theme={uploadState === "success" ? "primary" : "light"}
|
||||
text="Back to Overview"
|
||||
text={$at("Back to Overview")}
|
||||
onClick={onBack}
|
||||
/>
|
||||
)}
|
||||
@@ -1990,6 +2038,7 @@ function PreUploadedImageItem({
|
||||
onDelete: () => void;
|
||||
onContinueUpload: () => void;
|
||||
}) {
|
||||
const { $at }= useReactAt();
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
return (
|
||||
<label
|
||||
@@ -2034,7 +2083,7 @@ function PreUploadedImageItem({
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={TrashIcon}
|
||||
text="Delete"
|
||||
text={$at("Delete")}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
@@ -2055,7 +2104,7 @@ function PreUploadedImageItem({
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Continue uploading"
|
||||
text={$at("Continue uploading")}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onContinueUpload();
|
||||
@@ -2087,9 +2136,12 @@ function UsbModeSelector({
|
||||
usbMode: RemoteVirtualMediaState["mode"];
|
||||
setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
return (
|
||||
<div className="flex flex-col items-start space-y-1 select-none">
|
||||
<label className="text-sm font-semibold text-black dark:text-white">Mount as</label>
|
||||
<label className="text-sm font-semibold text-black dark:text-white">
|
||||
{ $at("Mount as") }
|
||||
</label>
|
||||
<div className="flex space-x-4">
|
||||
<label htmlFor="cdrom" className="flex items-center">
|
||||
<input
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
import { UploadDialog } from "@/components/UploadDialog";
|
||||
import { sync } from "framer-motion";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function MtpRoute() {
|
||||
const navigate = useNavigate();
|
||||
@@ -166,32 +166,33 @@ function MtpModeSelectionView({
|
||||
selectedMode: "mtp_device" | "mtp_sd";
|
||||
setSelectedMode: (mode: "mtp_device" | "mtp_sd") => void;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
const { setModalView } = useMountMediaStore();
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="animate-fadeIn space-y-0 opacity-0">
|
||||
<h2 className="text-lg leading-tight font-bold dark:text-white">
|
||||
Virtual Media Source
|
||||
{$at("Shared Folders")}
|
||||
</h2>
|
||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||
Choose how you want to mount your virtual media
|
||||
{$at("Select the shared folder that you want to manage")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{[
|
||||
{
|
||||
label: "KVM Storage Manager",
|
||||
label: "KVM Storage",
|
||||
value: "mtp_device",
|
||||
description: "Manage the shared folder located on eMMC",
|
||||
description: "",
|
||||
icon: LuRadioReceiver,
|
||||
tag: null,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "KVM MicroSD Manager",
|
||||
label: "KVM MicroSD",
|
||||
value: "mtp_sd",
|
||||
description: "Manage the shared folder located on MicroSD",
|
||||
description: "",
|
||||
icon: LuRadioReceiver,
|
||||
tag: null,
|
||||
disabled: false,
|
||||
@@ -259,14 +260,14 @@ function MtpModeSelectionView({
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<Button size="MD" theme="blank" onClick={onClose} text="Cancel" />
|
||||
<Button size="MD" theme="blank" onClick={onClose} text={$at("Cancel")} />
|
||||
<Button
|
||||
size="MD"
|
||||
theme="primary"
|
||||
onClick={() => {
|
||||
setModalView(selectedMode);
|
||||
}}
|
||||
text="Continue"
|
||||
text={$at("Continue")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,7 +289,7 @@ function DeviceFileView({
|
||||
createdAt: string;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const { $at }= useReactAt();
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const filesPerPage = 5;
|
||||
@@ -402,8 +403,8 @@ function DeviceFileView({
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from KVM Storage"
|
||||
description="Select an image to mount from the KVM storage"
|
||||
title={$at("Manage Shared Folders in KVM Storage")}
|
||||
description=""
|
||||
/>
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
@@ -419,17 +420,17 @@ function DeviceFileView({
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
No images available
|
||||
{$at("No files")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Upload an image to start virtual media mounting.
|
||||
{$at("Upload a new file")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Upload a new File"
|
||||
text={$at("Upload a New File")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -512,7 +513,7 @@ function DeviceFileView({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="blank" text={$at("Back")} onClick={() => onBack()} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -524,7 +525,7 @@ function DeviceFileView({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="MD" theme="light" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="light" text={$at("Back")} onClick={() => onBack()} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -538,10 +539,10 @@ function DeviceFileView({
|
||||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Available Storage
|
||||
{$at("Available Storage")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{percentageUsed}% used
|
||||
{percentageUsed}% {$at("used")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700">
|
||||
@@ -552,10 +553,10 @@ function DeviceFileView({
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesUsed)} used
|
||||
{formatters.bytes(bytesUsed)} {$at("used")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesFree)} free
|
||||
{formatters.bytes(bytesFree)} {$at("free")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -572,7 +573,7 @@ function DeviceFileView({
|
||||
size="MD"
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new File"
|
||||
text={$at("Upload a New File")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -715,6 +716,7 @@ function SDFileView({
|
||||
a.remove();
|
||||
}
|
||||
|
||||
const { $at }= useReactAt();
|
||||
const indexOfLastFile = currentPage * filesPerPage;
|
||||
const indexOfFirstFile = indexOfLastFile - filesPerPage;
|
||||
const currentFiles = onStorageFiles.slice(indexOfFirstFile, indexOfLastFile);
|
||||
@@ -777,8 +779,8 @@ function SDFileView({
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from KVM MicroSD Card"
|
||||
description="Select an image to mount from the KVM storage"
|
||||
title={$at("Manage Shared Folder in KVM Storage")}
|
||||
description=""
|
||||
/>
|
||||
<div className="flex items-center justify-center py-8 text-center">
|
||||
<div className="space-y-3">
|
||||
@@ -809,8 +811,8 @@ function SDFileView({
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<ViewHeader
|
||||
title="Mount from KVM MicroSD Card"
|
||||
description="Select an image to mount from the KVM storage"
|
||||
title={$at("Manage Shared Folder in KVM Storage")}
|
||||
description=""
|
||||
/>
|
||||
<div
|
||||
className="w-full animate-fadeIn opacity-0"
|
||||
@@ -826,10 +828,10 @@ function SDFileView({
|
||||
<div className="space-y-1">
|
||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
||||
No images available
|
||||
{$at("No files")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Upload a file.
|
||||
{$at("Upload a new file")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -837,7 +839,7 @@ function SDFileView({
|
||||
size="SM"
|
||||
disabled={loading}
|
||||
theme="primary"
|
||||
text="Upload a new File"
|
||||
text={$at("Upload a New File")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -858,7 +860,7 @@ function SDFileView({
|
||||
if (!selectedFile) return;
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to download " + selectedFile.name + "?",
|
||||
$at("Are you sure you want to download " + selectedFile.name + "?"),
|
||||
)
|
||||
) {
|
||||
handleSDDownloadFile(selectedFile);
|
||||
@@ -867,7 +869,7 @@ function SDFileView({
|
||||
onDelete={() => {
|
||||
const selectedFile = onStorageFiles.find(f => f.name === file.name);
|
||||
if (!selectedFile) return;
|
||||
if (window.confirm("Are you sure you want to delete " + selectedFile.name + "?")) {
|
||||
if (window.confirm($at("Are you sure you want to delete " + selectedFile.name + "?"))) {
|
||||
handleSDDeleteFile(selectedFile);
|
||||
}
|
||||
}}
|
||||
@@ -916,7 +918,7 @@ function SDFileView({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="MD" theme="blank" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="blank" text={$at("Back")} onClick={() => onBack()} />
|
||||
<Button
|
||||
size="MD"
|
||||
disabled={loading}
|
||||
@@ -935,7 +937,7 @@ function SDFileView({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 ml-auto ml-auto">
|
||||
<Button size="MD" theme="light" text="Back" onClick={() => onBack()} />
|
||||
<Button size="MD" theme="light" text={$at("Back")} onClick={() => onBack()} />
|
||||
<Button
|
||||
size="MD"
|
||||
theme="light"
|
||||
@@ -955,10 +957,10 @@ function SDFileView({
|
||||
>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Available Storage
|
||||
{$at("Available Storage")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{percentageUsed}% used
|
||||
{percentageUsed}% {$at("used")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3.5 w-full overflow-hidden rounded-sm bg-slate-200 dark:bg-slate-700">
|
||||
@@ -969,10 +971,10 @@ function SDFileView({
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesUsed)} used
|
||||
{formatters.bytes(bytesUsed)} {$at("used")}
|
||||
</span>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(bytesFree)} free
|
||||
{formatters.bytes(bytesFree)} {$at("free")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -990,7 +992,7 @@ function SDFileView({
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Upload a new File"
|
||||
text={$at("Upload a New File")}
|
||||
onClick={() => onNewImageClick()}
|
||||
/>
|
||||
</div>
|
||||
@@ -1008,7 +1010,7 @@ function SDFileView({
|
||||
disabled={loading}
|
||||
theme="light"
|
||||
fullWidth
|
||||
text="Unmount SD Card"
|
||||
text={$at("Unmount Micro SD Card")}
|
||||
onClick={() => handleUnmountSDStorage()}
|
||||
className="text-red-500 dark:text-red-400"
|
||||
/>
|
||||
@@ -1029,6 +1031,7 @@ function UploadFileView({
|
||||
incompleteFileName?: string;
|
||||
media?: string;
|
||||
}) {
|
||||
const { $at }= useReactAt();
|
||||
const [uploadState, setUploadState] = useState<"idle" | "uploading" | "success">(
|
||||
"idle",
|
||||
);
|
||||
@@ -1315,11 +1318,11 @@ function UploadFileView({
|
||||
<div className="w-full space-y-4">
|
||||
<UploadDialog
|
||||
open={true}
|
||||
title="Upload New Image"
|
||||
title={$at("Upload a New File")}
|
||||
description={
|
||||
incompleteFileName
|
||||
? `Continue uploading "${incompleteFileName}"`
|
||||
: "Select an image file to upload to KVM storage"
|
||||
? $at(`Continue uploading "${incompleteFileName}"`)
|
||||
: $at("Select a file to upload")
|
||||
}
|
||||
>
|
||||
<div
|
||||
@@ -1356,11 +1359,11 @@ function UploadFileView({
|
||||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
{incompleteFileName
|
||||
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
|
||||
: "Click to select a file"}
|
||||
? $at(`Click to select "${incompleteFileName.replace(".incomplete", "")}"`)
|
||||
: $at("Click to select a file")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
Do not support directory
|
||||
{$at("Do not support directories")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1375,7 +1378,7 @@ function UploadFileView({
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="leading-non text-lg font-semibold text-black dark:text-white">
|
||||
Uploading {formatters.truncateMiddle(uploadedFileName, 30)}
|
||||
{$at("Uploading")} {formatters.truncateMiddle(uploadedFileName, 30)}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.bytes(uploadedFileSize || 0)}
|
||||
@@ -1388,11 +1391,11 @@ function UploadFileView({
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-slate-600 dark:text-slate-400">
|
||||
<span>Uploading...</span>
|
||||
<span>{$at("Uploading...")}...</span>
|
||||
<span>
|
||||
{uploadSpeed !== null
|
||||
? `${formatters.bytes(uploadSpeed)}/s`
|
||||
: "Calculating..."}
|
||||
: $at("Calculating...")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1409,11 +1412,10 @@ function UploadFileView({
|
||||
</Card>
|
||||
</div>
|
||||
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||
Upload successful
|
||||
{$at("Upload Successful")}
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||
{formatters.truncateMiddle(uploadedFileName, 40)} has been
|
||||
uploaded
|
||||
{formatters.truncateMiddle(uploadedFileName, 40)} {$at("Uploaded")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1455,7 +1457,7 @@ function UploadFileView({
|
||||
<Button
|
||||
size="MD"
|
||||
theme="light"
|
||||
text="Cancel Upload"
|
||||
text={$at("Cancel Upload")}
|
||||
onClick={() => {
|
||||
onCancelUpload();
|
||||
setUploadState("idle");
|
||||
@@ -1532,6 +1534,7 @@ function PreUploadedImageItem({
|
||||
onDelete: () => void;
|
||||
onContinueUpload: () => void;
|
||||
}) {
|
||||
const { $at }= useReactAt();
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
return (
|
||||
<label
|
||||
@@ -1571,7 +1574,7 @@ function PreUploadedImageItem({
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuDownload}
|
||||
text="Download"
|
||||
text={$at("Download")}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDownload();
|
||||
@@ -1588,7 +1591,7 @@ function PreUploadedImageItem({
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={TrashIcon}
|
||||
text="Delete"
|
||||
text={$at("Delete")}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
@@ -1600,7 +1603,7 @@ function PreUploadedImageItem({
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Continue uploading"
|
||||
text={$at("Continue Uploading")}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onContinueUpload();
|
||||
|
||||
@@ -23,6 +23,11 @@ import { useVpnStore } from "@/hooks/stores";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
|
||||
import { LogDialog } from "../components/LogDialog";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
|
||||
import AutoHeight from "@/components/AutoHeight";
|
||||
|
||||
|
||||
export interface TailScaleResponse {
|
||||
state: string;
|
||||
@@ -41,6 +46,16 @@ export interface FrpcResponse {
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export interface EasyTierRunningResponse {
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export interface EasyTierResponse {
|
||||
name: string;
|
||||
secret: string;
|
||||
node: string;
|
||||
}
|
||||
|
||||
|
||||
export interface TLSState {
|
||||
mode: "self-signed" | "custom" | "disabled";
|
||||
@@ -59,6 +74,7 @@ const loader = async () => {
|
||||
};
|
||||
|
||||
export default function SettingsAccessIndexRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const loaderData = useLoaderData() as LocalDevice | null;
|
||||
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
@@ -94,7 +110,23 @@ export default function SettingsAccessIndexRoute() {
|
||||
const [frpcToml, setFrpcToml] = useState<string>("");
|
||||
const [frpcLog, setFrpcLog] = useState<string>("");
|
||||
const [showFrpcLogModal, setShowFrpcLogModal] = useState(false);
|
||||
const [frpcStatus, setFrpcRunningStatus] = useState<FrpcResponse>({ running: false });
|
||||
const [frpcRunningStatus, setFrpcRunningStatus] = useState<FrpcResponse>({ running: false });
|
||||
|
||||
const [tempEasyTierNetworkName, setTempEasyTierNetworkName] = useState("");
|
||||
const [tempEasyTierNetworkSecret, setTempEasyTierNetworkSecret] = useState("");
|
||||
const [tempEasyTierNetworkNodeMode, setTempEasyTierNetworkNodeMode] = useState("default");
|
||||
const [tempEasyTierNetworkNode, setTempEasyTierNetworkNode] = useState("tcp://public.easytier.cn:11010");
|
||||
const [easyTierRunningStatus, setEasyTierRunningStatus] = useState<EasyTierRunningResponse>({ running: false });
|
||||
const [showEasyTierLogModal, setShowEasyTierLogModal] = useState(false);
|
||||
const [showEasyTierNodeInfoModal, setShowEasyTierNodeInfoModal] = useState(false);
|
||||
const [easyTierLog, setEasyTierLog] = useState<string>("");
|
||||
const [easyTierNodeInfo, setEasyTierNodeInfo] = useState<string>("");
|
||||
const [easyTierConfig, setEasyTierConfig] = useState<EasyTierResponse>({
|
||||
name: "",
|
||||
secret: "",
|
||||
node: "",
|
||||
});
|
||||
|
||||
|
||||
const getTLSState = useCallback(() => {
|
||||
send("getTLSState", {}, resp => {
|
||||
@@ -207,12 +239,12 @@ export default function SettingsAccessIndexRoute() {
|
||||
});
|
||||
},[send]);
|
||||
|
||||
const handleTailScaleCanel = useCallback(() => {
|
||||
const handleTailScaleCancel = useCallback(() => {
|
||||
setIsDisconnecting(true);
|
||||
send("canelTailScale", {}, resp => {
|
||||
send("cancelTailScale", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to logout TailScale: ${resp.error.data || "Unknown error"}`,
|
||||
`Failed to cancel TailScale: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setIsDisconnecting(false);
|
||||
return;
|
||||
@@ -289,7 +321,7 @@ export default function SettingsAccessIndexRoute() {
|
||||
}, [send, frpcToml]);
|
||||
|
||||
const handleStopFrpc = useCallback(() => {
|
||||
send("stopFrpc", { frpcToml }, resp => {
|
||||
send("stopFrpc", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to stop frpc: ${resp.error.data || "Unknown error"}`,
|
||||
@@ -345,25 +377,133 @@ export default function SettingsAccessIndexRoute() {
|
||||
getFrpcToml();
|
||||
}, [getFrpcStatus, getFrpcToml]);
|
||||
|
||||
const handleStartEasyTier = useCallback(() => {
|
||||
if (!tempEasyTierNetworkName || !tempEasyTierNetworkSecret || !tempEasyTierNetworkNode) {
|
||||
notifications.error("Please enter EasyTier network name, secret and node");
|
||||
return;
|
||||
}
|
||||
setEasyTierConfig({
|
||||
name: tempEasyTierNetworkName,
|
||||
secret: tempEasyTierNetworkSecret,
|
||||
node: tempEasyTierNetworkNode,
|
||||
});
|
||||
send("startEasyTier", { name: tempEasyTierNetworkName, secret: tempEasyTierNetworkSecret, node: tempEasyTierNetworkNode }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to start EasyTier: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setEasyTierRunningStatus({ running: false });
|
||||
return;
|
||||
}
|
||||
notifications.success("EasyTier started");
|
||||
setEasyTierRunningStatus({ running: true });
|
||||
});
|
||||
}, [send, tempEasyTierNetworkName, tempEasyTierNetworkSecret, tempEasyTierNetworkNode]);
|
||||
|
||||
const handleStopEasyTier = useCallback(() => {
|
||||
send("stopEasyTier", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to stop EasyTier: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("EasyTier stopped");
|
||||
setEasyTierRunningStatus({ running: false });
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleGetEasyTierLog = useCallback(() => {
|
||||
send("getEasyTierLog", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get EasyTier log: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setEasyTierLog("");
|
||||
return;
|
||||
}
|
||||
setEasyTierLog(resp.result as string);
|
||||
setShowEasyTierLogModal(true);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleGetEasyTierNodeInfo = useCallback(() => {
|
||||
send("getEasyTierNodeInfo", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get EasyTier Node Info: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setEasyTierNodeInfo("");
|
||||
return;
|
||||
}
|
||||
setEasyTierNodeInfo(resp.result as string);
|
||||
setShowEasyTierNodeInfoModal(true);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const getEasyTierConfig = useCallback(() => {
|
||||
send("getEasyTierConfig", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get EasyTier config: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = resp.result as EasyTierResponse;
|
||||
setEasyTierConfig({
|
||||
name: result.name,
|
||||
secret: result.secret,
|
||||
node: result.node,
|
||||
});
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const getEasyTierStatus = useCallback(() => {
|
||||
console.log("getEasyTierStatus")
|
||||
send("getEasyTierStatus", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to get EasyTier status: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setEasyTierRunningStatus(resp.result as EasyTierRunningResponse);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
useEffect(() => {
|
||||
getEasyTierConfig();
|
||||
getEasyTierStatus();
|
||||
}, [getEasyTierStatus, getEasyTierConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tempEasyTierNetworkNodeMode === 'default') {
|
||||
setTempEasyTierNetworkNode('tcp://public.easytier.cn:11010');
|
||||
} else {
|
||||
setTempEasyTierNetworkNode('');
|
||||
}
|
||||
}, [tempEasyTierNetworkNodeMode]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Access"
|
||||
description="Manage the Access Control of the device"
|
||||
title={$at("Access")}
|
||||
description={$at("Manage the Access Control of the device")}
|
||||
/>
|
||||
|
||||
{loaderData?.authMode && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Local"
|
||||
description="Manage the mode of local access to the device"
|
||||
title={$at("Local")}
|
||||
description={$at("Manage the mode of local access to the device")}
|
||||
/>
|
||||
<>
|
||||
<SettingsItem
|
||||
title="HTTPS Mode"
|
||||
title={$at("HTTPS Mode")}
|
||||
badge="Experimental"
|
||||
description="Configure secure HTTPS access to your device"
|
||||
description={$at("Configure secure HTTPS access to your device")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -371,9 +511,9 @@ export default function SettingsAccessIndexRoute() {
|
||||
onChange={e => handleTlsModeChange(e.target.value)}
|
||||
disabled={tlsMode === "unknown"}
|
||||
options={[
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "self-signed", label: "Self-signed" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
{ value: "disabled", label: $at("Disabled") },
|
||||
{ value: "self-signed", label: $at("Self-signed") },
|
||||
{ value: "custom", label: $at("Custom") },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
@@ -382,15 +522,15 @@ export default function SettingsAccessIndexRoute() {
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="TLS Certificate"
|
||||
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)."
|
||||
title={$at("TLS Certificate")}
|
||||
description={$at("Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates).")}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Certificate"
|
||||
label={$at("Certificate")}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
$at("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----")
|
||||
}
|
||||
value={tlsCert}
|
||||
onChange={e => handleTlsCertChange(e.target.value)}
|
||||
@@ -400,11 +540,11 @@ export default function SettingsAccessIndexRoute() {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Private Key"
|
||||
description="For security reasons, it will not be displayed after saving."
|
||||
label={$at("Private Key")}
|
||||
description={$at("For security reasons, it will not be displayed after saving.")}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
$at("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----")
|
||||
}
|
||||
value={tlsKey}
|
||||
onChange={e => handleTlsKeyChange(e.target.value)}
|
||||
@@ -416,7 +556,7 @@ export default function SettingsAccessIndexRoute() {
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update TLS Settings"
|
||||
text={$at("Update TLS Settings")}
|
||||
onClick={handleCustomTlsUpdate}
|
||||
/>
|
||||
</div>
|
||||
@@ -424,14 +564,14 @@ export default function SettingsAccessIndexRoute() {
|
||||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Authentication Mode"
|
||||
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`}
|
||||
title={$at("Authentication Mode")}
|
||||
description={`${$at("Current mode:")} ${loaderData.authMode === "password" ? $at("Password protected") : $at("No password")}`}
|
||||
>
|
||||
{loaderData.authMode === "password" ? (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Disable Protection"
|
||||
text={$at("Disable Protection")}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "deletePassword" } });
|
||||
}}
|
||||
@@ -440,7 +580,7 @@ export default function SettingsAccessIndexRoute() {
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable Password"
|
||||
text={$at("Enable Password")}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "createPassword" } });
|
||||
}}
|
||||
@@ -451,13 +591,13 @@ export default function SettingsAccessIndexRoute() {
|
||||
|
||||
{loaderData.authMode === "password" && (
|
||||
<SettingsItem
|
||||
title="Change Password"
|
||||
description="Update your device access password"
|
||||
title={$at("Change Password")}
|
||||
description={$at("Update your device access password")}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Change Password"
|
||||
text={$at("Change Password")}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "updatePassword" } });
|
||||
}}
|
||||
@@ -471,224 +611,407 @@ export default function SettingsAccessIndexRoute() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Remote"
|
||||
description="Manage the mode of Remote access to the device"
|
||||
title={$at("Remote")}
|
||||
description={$at("Manage the mode of Remote access to the device")}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Add TailScale settings item */}
|
||||
<SettingsItem
|
||||
title="TailScale"
|
||||
badge="Experimental"
|
||||
description="Connect to TailScale VPN network"
|
||||
>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title=""
|
||||
description="TailScale use xEdge server"
|
||||
>
|
||||
<Checkbox
|
||||
checked={tailScaleXEdge}
|
||||
onChange={e => {
|
||||
if (tailScaleConnectionState !== "disconnected") {
|
||||
notifications.error("TailScale is running and this setting cannot be modified");
|
||||
return;
|
||||
}
|
||||
handleTailScaleXEdgeChange(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</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">
|
||||
{tailScaleConnectionState === "connecting" && (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<p>Connecting...</p>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Canel"
|
||||
onClick={handleTailScaleCanel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{tailScaleConnectionState === "connected" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
{tailScaleLoginUrl && (
|
||||
<p>Login URL: <a href={tailScaleLoginUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400">LoginUrl</a></p>
|
||||
)}
|
||||
{!tailScaleLoginUrl && (
|
||||
<p>Wait to obtain the Login URL</p>
|
||||
)}
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text= { isDisconnecting ? "Quitting..." : "Quit"}
|
||||
onClick={handleTailScaleLogout}
|
||||
disabled={ isDisconnecting === true }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tailScaleConnectionState === "logined" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
<p>IP: {tailScaleIP}</p>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text= { isDisconnecting ? "Quitting..." : "Quit"}
|
||||
onClick={handleTailScaleLogout}
|
||||
disabled={ isDisconnecting === true }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tailScaleConnectionState === "closed" && (
|
||||
<div className="text-sm text-red-600 dark:text-red-400">
|
||||
<p>Connect fail, please retry</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Add ZeroTier settings item */}
|
||||
<SettingsItem
|
||||
title="ZeroTier"
|
||||
badge="Experimental"
|
||||
description="Connect to ZeroTier VPN network"
|
||||
>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{zeroTierConnectionState === "connecting" && (
|
||||
<div className="text-sm text-slate-700 dark:text-slate-300">
|
||||
<p>Connecting...</p>
|
||||
</div>
|
||||
)}
|
||||
{zeroTierConnectionState === "connected" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
<p>Network ID: {zeroTierNetworkID}</p>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Quit"
|
||||
onClick={handleZeroTierLogout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{zeroTierConnectionState === "logined" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
<p>Network ID: {zeroTierNetworkID}</p>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Quit"
|
||||
onClick={handleZeroTierLogout}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
<p>Network IP: {zeroTierIP}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{zeroTierConnectionState === "closed" && (
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
<p>Connect fail, please retry</p>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Retry"
|
||||
onClick={handleZeroTierLogout}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{(zeroTierConnectionState === "disconnected") && (
|
||||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Network ID"
|
||||
value={tempNetworkID}
|
||||
onChange={handleZeroTierNetworkIdChange}
|
||||
placeholder="Enter ZeroTier Network ID"
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Join in"
|
||||
onClick={handleZeroTierLogin}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</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}
|
||||
<div className="space-y-4">
|
||||
{/* Add TailScale settings item */}
|
||||
<SettingsItem
|
||||
title="TailScale"
|
||||
badge="Experimental"
|
||||
description={$at("Connect to TailScale VPN network")}
|
||||
>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<AutoHeight>
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title=""
|
||||
description={$at("TailScale use xEdge server")}
|
||||
>
|
||||
<Checkbox disabled={tailScaleConnectionState !== "disconnected"}
|
||||
checked={tailScaleXEdge}
|
||||
onChange={e => {
|
||||
if (tailScaleConnectionState !== "disconnected") {
|
||||
notifications.error("TailScale is running and this setting cannot be modified");
|
||||
return;
|
||||
}
|
||||
handleTailScaleXEdgeChange(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{tailScaleConnectionState === "connecting" && (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<p>Connecting...</p>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Cancel")}
|
||||
onClick={handleTailScaleCancel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{tailScaleConnectionState === "connected" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
{tailScaleLoginUrl && (
|
||||
<p>{$at("Login URL:")} <a href={tailScaleLoginUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400">LoginUrl</a></p>
|
||||
)}
|
||||
{!tailScaleLoginUrl && (
|
||||
<p>{$at("Wait to obtain the Login URL")}</p>
|
||||
)}
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text= { isDisconnecting ? $at("Quitting...") : $at("Quit")}
|
||||
onClick={handleTailScaleLogout}
|
||||
disabled={ isDisconnecting === true }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tailScaleConnectionState === "logined" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{$at("Network IP")}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{tailScaleIP}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text= { isDisconnecting ? $at("Quitting...") : $at("Quit")}
|
||||
onClick={handleTailScaleLogout}
|
||||
disabled={ isDisconnecting === true }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tailScaleConnectionState === "closed" && (
|
||||
<div className="text-sm text-red-600 dark:text-red-400">
|
||||
<p>Connect fail, please retry</p>
|
||||
</div>
|
||||
)}
|
||||
{ ((tailScaleConnectionState === "disconnected") || (tailScaleConnectionState === "closed")) && (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Log"
|
||||
onClick={handleGetFrpcLog}
|
||||
text={$at("Enable")}
|
||||
onClick={handleTailScaleLogin}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Start frpc"
|
||||
onClick={handleStartFrpc}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
</AutoHeight>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Add ZeroTier settings item */}
|
||||
<SettingsItem
|
||||
title="ZeroTier"
|
||||
badge="Experimental"
|
||||
description={$at("Connect to ZeroTier VPN network")}
|
||||
>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<AutoHeight>
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
{zeroTierConnectionState === "connecting" && (
|
||||
<div className="text-sm text-slate-700 dark:text-slate-300">
|
||||
<p>{$at("Connecting...")}</p>
|
||||
</div>
|
||||
)}
|
||||
{zeroTierConnectionState === "connected" && (
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{$at("Network ID")}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{zeroTierNetworkID}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Quit")}
|
||||
onClick={handleZeroTierLogout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{zeroTierConnectionState === "logined" && (
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{$at("Network ID")}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{zeroTierNetworkID}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{$at("Network IP")}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{zeroTierIP}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Quit")}
|
||||
onClick={handleZeroTierLogout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{zeroTierConnectionState === "closed" && (
|
||||
<div className="flex items-center gap-x-2 justify-between">
|
||||
<p>{$at("Connect fail, please retry")}</p>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Retry")}
|
||||
onClick={handleZeroTierLogout}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(zeroTierConnectionState === "disconnected") && (
|
||||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label={$at("Network ID")}
|
||||
value={tempNetworkID}
|
||||
onChange={handleZeroTierNetworkIdChange}
|
||||
placeholder={$at("Enter ZeroTier Network ID")}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Join in")}
|
||||
onClick={handleZeroTierLogin}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
</AutoHeight>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="EasyTier"
|
||||
description={$at("Connect to EasyTier server")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutoHeight>
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
{ easyTierRunningStatus.running ? (
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{$at("Network Name")}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{easyTierConfig.name || tempEasyTierNetworkName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{$at("Network Secret")}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{easyTierConfig.secret || tempEasyTierNetworkSecret}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{$at("Network Node")}
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{easyTierConfig.node || tempEasyTierNetworkNode}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text={$at("Stop")}
|
||||
onClick={handleStopEasyTier}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Log")}
|
||||
onClick={handleGetEasyTierLog}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Node Info")}
|
||||
onClick={handleGetEasyTierNodeInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label={$at("Network Name")}
|
||||
value={tempEasyTierNetworkName}
|
||||
onChange={e => setTempEasyTierNetworkName(e.target.value)}
|
||||
placeholder={$at("Enter EasyTier Network Name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label={$at("Network Secret")}
|
||||
value={tempEasyTierNetworkSecret}
|
||||
onChange={e => setTempEasyTierNetworkSecret(e.target.value)}
|
||||
placeholder={$at("Enter EasyTier Network Secret")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={$at("Network Node")}
|
||||
description=""
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={tempEasyTierNetworkNodeMode}
|
||||
onChange={e => setTempEasyTierNetworkNodeMode(e.target.value)}
|
||||
options={[
|
||||
{ value: "default", label: $at("Default") },
|
||||
{ value: "custom", label: $at("Custom") },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
{tempEasyTierNetworkNodeMode === "custom" && (
|
||||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label={$at("Network Node")}
|
||||
value={tempEasyTierNetworkNode}
|
||||
onChange={e => setTempEasyTierNetworkNode(e.target.value)}
|
||||
placeholder={$at("Enter EasyTier Network Node")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={$at("Start")}
|
||||
onClick={handleStartEasyTier}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</GridCard>
|
||||
</AutoHeight>
|
||||
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Frp"
|
||||
description={$at("Connect to Frp server")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutoHeight>
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label={$at("Edit frpc.toml")}
|
||||
placeholder={$at("Enter frpc configuration")}
|
||||
value={frpcToml || ""}
|
||||
rows={3}
|
||||
readOnly={frpcRunningStatus.running}
|
||||
onChange={e => setFrpcToml(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{ frpcRunningStatus.running ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text={$at("Stop")}
|
||||
onClick={handleStopFrpc}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={$at("Log")}
|
||||
onClick={handleGetFrpcLog}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={$at("Start")}
|
||||
onClick={handleStartFrpc}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
</AutoHeight>
|
||||
|
||||
</div>
|
||||
|
||||
<LogDialog
|
||||
open={showEasyTierLogModal}
|
||||
onClose={() => {
|
||||
setShowEasyTierLogModal(false);
|
||||
}}
|
||||
title="EasyTier Log"
|
||||
description={easyTierLog}
|
||||
/>
|
||||
|
||||
<LogDialog
|
||||
open={showEasyTierNodeInfoModal}
|
||||
onClose={() => {
|
||||
setShowEasyTierNodeInfoModal(false);
|
||||
}}
|
||||
title="EasyTier Node Info"
|
||||
description={easyTierNodeInfo}
|
||||
/>
|
||||
|
||||
<LogDialog
|
||||
open={showFrpcLogModal}
|
||||
onClose={() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import api from "@/api";
|
||||
import { useLocalAuthModalStore } from "@/hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { useReactAt } from "i18n-auto-extractor/react";
|
||||
|
||||
export default function SecurityAccessLocalAuthRoute() {
|
||||
const { setModalView } = useLocalAuthModalStore();
|
||||
@@ -28,6 +29,7 @@ export default function SecurityAccessLocalAuthRoute() {
|
||||
}
|
||||
|
||||
export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
const { $at } = useReactAt();
|
||||
const { modalView, setModalView } = useLocalAuthModalStore();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const revalidator = useRevalidator();
|
||||
@@ -65,17 +67,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
confirmNewPassword: string,
|
||||
) => {
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setError("Passwords do not match");
|
||||
setError($at("Passwords do not match"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPassword === "") {
|
||||
setError("Please enter your old password");
|
||||
setError($at("Please enter your old password"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword === "") {
|
||||
setError("Please enter a new password");
|
||||
setError($at("Please enter a new password"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,17 +93,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while changing the password");
|
||||
setError(data.error || $at("An error occurred while changing the password"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while changing the password");
|
||||
setError($at("An error occurred while changing the password"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePassword = async (password: string) => {
|
||||
if (password === "") {
|
||||
setError("Please enter your current password");
|
||||
setError($at("Please enter your current password"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,11 +115,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while disabling the password");
|
||||
setError(data.error || $at("An error occurred while disabling the password"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while disabling the password");
|
||||
setError($at("An error occurred while disabling the password"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,24 +152,24 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||
|
||||
{modalView === "creationSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Set Successfully"
|
||||
description="You've successfully set up local device protection. Your device is now secure against unauthorized local access."
|
||||
headline={$at("Password Set Successfully")}
|
||||
description={$at("You've successfully set up local device protection. Your device is now secure against unauthorized local access.")}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "deleteSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Protection Disabled"
|
||||
description="You've successfully disabled the password protection for local access. Remember, your device is now less secure."
|
||||
headline={$at("Password Protection Disabled")}
|
||||
description={$at("You've successfully disabled the password protection for local access. Remember, your device is now less secure.")}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "updateSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Updated Successfully"
|
||||
description="You've successfully changed your local device protection password. Make sure to remember your new password for future access."
|
||||
headline={$at("Password Updated Successfully")}
|
||||
description={$at("You've successfully changed your local device protection password. Make sure to remember your new password for future access.")}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
@@ -185,6 +187,7 @@ function CreatePasswordModal({
|
||||
onCancel: () => void;
|
||||
error: string | null;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
|
||||
@@ -198,24 +201,24 @@ function CreatePasswordModal({
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Local Device Protection
|
||||
{$at("Local Device Protection")}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Create a password to protect your device from unauthorized local access.
|
||||
{$at("Create a password to protect your device from unauthorized local access.")}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="New Password"
|
||||
label={$at("New Password")}
|
||||
type="password"
|
||||
placeholder="Enter a strong password"
|
||||
placeholder={$at("Enter a strong password")}
|
||||
value={password}
|
||||
autoFocus
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm New Password"
|
||||
label={$at("Confirm New Password")}
|
||||
type="password"
|
||||
placeholder="Re-enter your password"
|
||||
placeholder={$at("Re-enter your password")}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
@@ -224,10 +227,10 @@ function CreatePasswordModal({
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Secure Device"
|
||||
text={$at("Secure Device")}
|
||||
onClick={() => onSetPassword(password, confirmPassword)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Not Now" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={$at("Not Now")} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</form>
|
||||
@@ -244,6 +247,7 @@ function DeletePasswordModal({
|
||||
onCancel: () => void;
|
||||
error: string | null;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
return (
|
||||
@@ -251,16 +255,16 @@ function DeletePasswordModal({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Disable Local Device Protection
|
||||
{$at("Disable Local Device Protection")}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Enter your current password to disable local device protection.
|
||||
{$at("Enter your current password to disable local device protection.")}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="Current Password"
|
||||
label={$at("Current Password")}
|
||||
type="password"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={$at("Enter your current password")}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
@@ -268,10 +272,10 @@ function DeletePasswordModal({
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Disable Protection"
|
||||
text={$at("Disable Protection")}
|
||||
onClick={() => onDeletePassword(password)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={$at("Cancel")} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</div>
|
||||
@@ -292,6 +296,7 @@ function UpdatePasswordModal({
|
||||
onCancel: () => void;
|
||||
error: string | null;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState("");
|
||||
@@ -306,31 +311,30 @@ function UpdatePasswordModal({
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Change Local Device Password
|
||||
{ $at("Change Local Device Password") }
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Enter your current password and a new password to update your local device
|
||||
protection.
|
||||
{ $at("Enter your current password and a new password to update your local device protection.") }
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="Current Password"
|
||||
label={ $at("Current Password") }
|
||||
type="password"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={ $at("Enter your current password") }
|
||||
value={oldPassword}
|
||||
onChange={e => setOldPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="New Password"
|
||||
label={ $at("New Password") }
|
||||
type="password"
|
||||
placeholder="Enter a new strong password"
|
||||
placeholder={ $at("Enter a new strong password") }
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm New Password"
|
||||
label={ $at("Confirm New Password") }
|
||||
type="password"
|
||||
placeholder="Re-enter your new password"
|
||||
placeholder={ $at("Re-enter your new password") }
|
||||
value={confirmNewPassword}
|
||||
onChange={e => setConfirmNewPassword(e.target.value)}
|
||||
/>
|
||||
@@ -338,10 +342,10 @@ function UpdatePasswordModal({
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update Password"
|
||||
text={ $at("Update Password") }
|
||||
onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={ $at("Cancel") } onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</form>
|
||||
@@ -358,6 +362,7 @@ function SuccessModal({
|
||||
description: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="space-y-4">
|
||||
@@ -365,7 +370,7 @@ function SuccessModal({
|
||||
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
|
||||
</div>
|
||||
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={ $at("Close") } onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,8 +13,10 @@ import { isOnDevice } from "../main";
|
||||
import notifications from "../notifications";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function SettingsAdvancedRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
const [sshKey, setSSHKey] = useState<string>("");
|
||||
@@ -135,14 +137,14 @@ export default function SettingsAdvancedRoute() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Advanced"
|
||||
description="Access additional settings for troubleshooting and customization"
|
||||
title={$at("Advanced")}
|
||||
description={$at("Access additional settings for troubleshooting and customization")}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Loopback-Only Mode"
|
||||
description="Restrict web interface access to localhost only (127.0.0.1)"
|
||||
title={$at("Loopback-Only Mode")}
|
||||
description={$at("Restrict web interface access to localhost only (127.0.0.1)")}
|
||||
>
|
||||
<Checkbox
|
||||
checked={localLoopbackOnly}
|
||||
@@ -153,25 +155,25 @@ export default function SettingsAdvancedRoute() {
|
||||
{isOnDevice && (
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="SSH Access"
|
||||
description="Add your SSH public key to enable secure remote access to the device"
|
||||
title={$at("SSH Access")}
|
||||
description={$at("Add your SSH public key to enable secure remote access to the device")}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="SSH Public Key"
|
||||
label={$at("SSH Public Key")}
|
||||
value={sshKey || ""}
|
||||
rows={3}
|
||||
onChange={e => setSSHKey(e.target.value)}
|
||||
placeholder="Enter your SSH public key"
|
||||
placeholder={$at("Enter your SSH public key")}
|
||||
/>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The default SSH user is <strong>root</strong>.
|
||||
{$at("The default SSH user is ")} <strong>root</strong>.
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update SSH Key"
|
||||
text={$at("Update SSH Key")}
|
||||
onClick={handleUpdateSSHKey}
|
||||
/>
|
||||
</div>
|
||||
@@ -180,8 +182,8 @@ export default function SettingsAdvancedRoute() {
|
||||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Troubleshooting Mode"
|
||||
description="Diagnostic tools and additional controls for troubleshooting and development purposes"
|
||||
title={$at("Troubleshooting Mode")}
|
||||
description={$at("Diagnostic tools and additional controls for troubleshooting and development purposes")}
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={settings.debugMode}
|
||||
@@ -194,27 +196,27 @@ export default function SettingsAdvancedRoute() {
|
||||
{settings.debugMode && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="USB Emulation"
|
||||
description="Control the USB emulation state"
|
||||
title={$at("USB Emulation")}
|
||||
description={$at("Control the USB emulation state")}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={
|
||||
usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation"
|
||||
usbEmulationEnabled ? $at("Disable USB Emulation") : $at("Enable USB Emulation")
|
||||
}
|
||||
onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Reset Configuration"
|
||||
description="Reset configuration to default. This will log you out."
|
||||
title={$at("Reset Configuration")}
|
||||
description={$at("Reset configuration to default. This will log you out.Some configuration changes will take effect after restart system.")}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reset Config"
|
||||
text={$at("Reset Config")}
|
||||
onClick={() => {
|
||||
handleResetConfig();
|
||||
window.location.reload();
|
||||
|
||||
@@ -4,8 +4,10 @@ import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function SettingsAppearanceRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const [currentTheme, setCurrentTheme] = useState(() => {
|
||||
return localStorage.theme || "system";
|
||||
});
|
||||
@@ -31,18 +33,18 @@ export default function SettingsAppearanceRoute() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Appearance"
|
||||
description="Customize the look and feel of your KVM interface"
|
||||
title={$at("Appearance")}
|
||||
description={$at("Customize the look and feel of your KVM interface")}
|
||||
/>
|
||||
<SettingsItem title="Theme" description="Choose your preferred color theme">
|
||||
<SettingsItem title={$at("Theme")} description={$at("Choose your preferred color theme")}>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={currentTheme}
|
||||
options={[
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
{ value: "system", label: $at("System") },
|
||||
{ value: "light", label: $at("Light") },
|
||||
{ value: "dark", label: $at("Dark") },
|
||||
]}
|
||||
onChange={e => {
|
||||
setCurrentTheme(e.target.value);
|
||||
|
||||
@@ -11,11 +11,13 @@ import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
import { useDeviceStore } from "../hooks/stores";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function SettingsGeneralRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const [autoUpdate, setAutoUpdate] = useState(true);
|
||||
const { $at } = useReactAt();
|
||||
|
||||
const currentVersions = useDeviceStore(state => {
|
||||
const { appVersion, systemVersion } = state;
|
||||
@@ -45,44 +47,44 @@ export default function SettingsGeneralRoute() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="General"
|
||||
description="Configure device settings and update preferences"
|
||||
title={$at("General")}
|
||||
description={$at("Configure device settings and update preferences")}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 pb-2">
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title="Version"
|
||||
title={$at("Version")}
|
||||
description={
|
||||
currentVersions ? (
|
||||
<>
|
||||
App: {currentVersions.appVersion}
|
||||
{$at("App")}: {currentVersions.appVersion}
|
||||
<br />
|
||||
System: {currentVersions.systemVersion}
|
||||
{$at("System")}: {currentVersions.systemVersion}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
App: Loading...
|
||||
{$at("App: Loading...")}
|
||||
<br />
|
||||
System: Loading...
|
||||
{$at("System: Loading...")}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
<Button className="hidden"
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Check for Updates"
|
||||
text={$at("Check for Updates")}
|
||||
onClick={() => navigateTo("./update")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden space-y-4">
|
||||
<SettingsItem
|
||||
title="Auto Update"
|
||||
description="Automatically update the device to the latest version"
|
||||
title={$at("Auto Update")}
|
||||
description={$at("Automatically update the device to the latest version")}
|
||||
>
|
||||
<Checkbox
|
||||
checked={autoUpdate}
|
||||
@@ -95,5 +97,5 @@ export default function SettingsGeneralRoute() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import { Button, LinkButton } from "@/components/Button";
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { UsbEpModeSetting } from "@components/UsbEpModeSetting";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const [send] = useJsonRpc();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
@@ -139,7 +141,6 @@ export default function SettingsHardwareRoute() {
|
||||
`Failed to get LED-Green mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
console.log("LED-Green mode:", resp.result);
|
||||
const result = resp.result as string;
|
||||
setLedGreenMode(result);
|
||||
});
|
||||
@@ -150,7 +151,6 @@ export default function SettingsHardwareRoute() {
|
||||
`Failed to get LED-Yellow mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
console.log("LED-Yellow mode:", resp.result);
|
||||
const result = resp.result as string;
|
||||
setLedYellowMode(result);
|
||||
});
|
||||
@@ -159,13 +159,13 @@ export default function SettingsHardwareRoute() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Hardware"
|
||||
description="Configure display settings and hardware options for your KVM device"
|
||||
title={$at("Hardware")}
|
||||
description={$at("Configure display settings and hardware options for your KVM device")}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Display Orientation"
|
||||
description="Set the orientation of the display"
|
||||
title={$at("Display Orientation")}
|
||||
description={$at("Set the orientation of the display")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -184,18 +184,18 @@ export default function SettingsHardwareRoute() {
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Display Brightness"
|
||||
description="Set the brightness of the display"
|
||||
title={$at("Display Brightness")}
|
||||
description={$at("Set the brightness of the display")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.max_brightness.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Off" },
|
||||
{ value: "64", label: "Low" },
|
||||
{ value: "128", label: "Medium" },
|
||||
{ value: "200", label: "High" },
|
||||
{ value: "0", label: $at("Off") },
|
||||
{ value: "64", label: $at("Low") },
|
||||
{ value: "128", label: $at("Medium") },
|
||||
{ value: "200", label: $at("High") },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.max_brightness = parseInt(e.target.value);
|
||||
@@ -215,8 +215,8 @@ export default function SettingsHardwareRoute() {
|
||||
{settings.backlightSettings.max_brightness != 0 && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="Dim Display After"
|
||||
description="Set how long to wait before dimming the display"
|
||||
title={$at("Dim Display After")}
|
||||
description={$at("Set how long to wait before dimming the display")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -237,8 +237,8 @@ export default function SettingsHardwareRoute() {
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Turn off Display After"
|
||||
description="Period of inactivity before display automatically turns off"
|
||||
title={$at("Turn off Display After")}
|
||||
description={$at("Period of inactivity before display automatically turns off")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -259,15 +259,15 @@ export default function SettingsHardwareRoute() {
|
||||
</SettingsItem>
|
||||
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The display will wake up when the connection state changes, or when touched.
|
||||
{$at("The display will wake up when the connection state changes, or when touched.")}
|
||||
</p>
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Time Zone"
|
||||
description="Set the time zone for the clock"
|
||||
title={$at("Time Zone")}
|
||||
description={$at("Set the time zone for the clock")}
|
||||
>
|
||||
</SettingsItem>
|
||||
<div className="space-y-4">
|
||||
@@ -281,25 +281,25 @@ export default function SettingsHardwareRoute() {
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Set"
|
||||
text={$at("Set")}
|
||||
onClick={handleTimeZoneSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsItem
|
||||
title="LED-Green Type"
|
||||
description="Set the type of system status indicated by the LED-Green"
|
||||
title={$at("LED-Green Type")}
|
||||
description={$at("Set the type of system status indicated by the LED-Green")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.ledGreenMode.toString()}
|
||||
options={[
|
||||
{ value: "network-link", label: "network-link" },
|
||||
{ value: "network-tx", label: "network-tx" },
|
||||
{ value: "network-rx", label: "network-rx" },
|
||||
{ value: "kernel-activity", label: "kernel-activity" },
|
||||
{ value: "network-link", label: $at("network-link") },
|
||||
{ value: "network-tx", label: $at("network-tx") },
|
||||
{ value: "network-rx", label: $at("network-rx") },
|
||||
{ value: "kernel-activity", label: $at("kernel-activity") },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.ledGreenMode = e.target.value;
|
||||
@@ -309,18 +309,18 @@ export default function SettingsHardwareRoute() {
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="LED-Yellow Type"
|
||||
description="Set the type of system status indicated by the LED-Yellow"
|
||||
title={$at("LED-Yellow Type")}
|
||||
description={$at("Set the type of system status indicated by the LED-Yellow")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.ledYellowMode.toString()}
|
||||
options={[
|
||||
{ value: "network-link", label: "network-link" },
|
||||
{ value: "network-tx", label: "network-tx" },
|
||||
{ value: "network-rx", label: "network-rx" },
|
||||
{ value: "kernel-activity", label: "kernel-activity" },
|
||||
{ value: "network-link", label: $at("network-link") },
|
||||
{ value: "network-tx", label: $at("network-tx") },
|
||||
{ value: "network-rx", label: $at("network-rx") },
|
||||
{ value: "kernel-activity", label: $at("kernel-activity") },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.ledYellowMode = e.target.value;
|
||||
|
||||
@@ -11,7 +11,10 @@ import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function SettingsKeyboardRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
||||
@@ -33,11 +36,6 @@ export default function SettingsKeyboardRoute() {
|
||||
}, [keyboardLayout]);
|
||||
|
||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||
const ledSyncOptions = [
|
||||
{ value: "auto", label: "Automatic" },
|
||||
{ value: "browser", label: "Browser Only" },
|
||||
{ value: "host", label: "Host Only" },
|
||||
];
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
@@ -67,15 +65,15 @@ export default function SettingsKeyboardRoute() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Keyboard"
|
||||
description="Configure keyboard settings for your device"
|
||||
title={$at("Keyboard")}
|
||||
description={$at("Configure keyboard settings for your device")}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
||||
<SettingsItem
|
||||
title="Paste text"
|
||||
description="Keyboard layout of target operating system"
|
||||
title={$at("Paste text")}
|
||||
description={$at("Keyboard layout of target operating system")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -87,15 +85,15 @@ export default function SettingsKeyboardRoute() {
|
||||
/>
|
||||
</SettingsItem>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in KVM matches the settings in the operating system.
|
||||
{$at("Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in KVM matches the settings in the operating system.")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
||||
<SettingsItem
|
||||
title="LED state synchronization"
|
||||
description="Synchronize the LED state of the keyboard with the target device"
|
||||
title={$at("LED state synchronization")}
|
||||
description={$at("Synchronize the LED state of the keyboard with the target device")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -103,15 +101,19 @@ export default function SettingsKeyboardRoute() {
|
||||
fullWidth
|
||||
value={keyboardLedSync}
|
||||
onChange={e => setKeyboardLedSync(e.target.value as KeyboardLedSync)}
|
||||
options={ledSyncOptions}
|
||||
options={[
|
||||
{ value: "auto", label: $at("Auto") },
|
||||
{ value: "browser", label: $at("Browser Only") },
|
||||
{ value: "host", label: $at("Host Only") },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Show Pressed Keys"
|
||||
description="Display currently pressed keys in the status bar"
|
||||
title={$at("Show Pressed Keys")}
|
||||
description={$at("Display currently pressed keys in the status bar")}
|
||||
>
|
||||
<Checkbox
|
||||
checked={showPressedKeys}
|
||||
|
||||
@@ -6,8 +6,10 @@ import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import { MacroForm } from "@/components/MacroForm";
|
||||
import { DEFAULT_DELAY } from "@/constants/macros";
|
||||
import notifications from "@/notifications";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function SettingsMacrosAddRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const { macros, saveMacros } = useMacrosStore();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -46,8 +48,8 @@ export default function SettingsMacrosAddRoute() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Add New Macro"
|
||||
description="Create a new keyboard macro"
|
||||
title={$at("Add New Macro")}
|
||||
description={$at("Create a new keyboard macro")}
|
||||
/>
|
||||
<MacroForm
|
||||
initialData={{
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MacroForm } from "@/components/MacroForm";
|
||||
import notifications from "@/notifications";
|
||||
import { Button } from "@/components/Button";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
@@ -17,6 +18,7 @@ const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
};
|
||||
|
||||
export default function SettingsMacrosEditRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const { macros, saveMacros } = useMacrosStore();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
@@ -95,13 +97,13 @@ export default function SettingsMacrosEditRoute() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<SettingsPageHeader
|
||||
title="Edit Macro"
|
||||
description="Modify your keyboard macro"
|
||||
title={$at("Edit Macro")}
|
||||
description={$at("Modify your keyboard macro")}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Delete Macro"
|
||||
text={$at("Delete Macro")}
|
||||
className="text-red-500 dark:text-red-400"
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
@@ -113,7 +115,7 @@ export default function SettingsMacrosEditRoute() {
|
||||
onSubmit={handleUpdateMacro}
|
||||
onCancel={() => navigate("../")}
|
||||
isSubmitting={isUpdating}
|
||||
submitText="Save Changes"
|
||||
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
@@ -122,7 +124,8 @@ export default function SettingsMacrosEditRoute() {
|
||||
title="Delete Macro"
|
||||
description="Are you sure you want to delete this macro? This action cannot be undone."
|
||||
variant="danger"
|
||||
confirmText={isDeleting ? "Deleting" : "Delete"}
|
||||
confirmText={isDeleting ? $at("Deleting") : $at("Delete")}
|
||||
cancelText={$at("Cancel")}
|
||||
onConfirm={() => {
|
||||
handleDeleteMacro();
|
||||
setShowDeleteConfirm(false);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings";
|
||||
import notifications from "@/notifications";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
@@ -30,6 +31,7 @@ const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
};
|
||||
|
||||
export default function SettingsMacrosRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore();
|
||||
const navigate = useNavigate();
|
||||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||
@@ -223,7 +225,7 @@ export default function SettingsMacrosRoute() {
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">
|
||||
Delay only
|
||||
{$at("Delay Only")}
|
||||
</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
@@ -264,7 +266,7 @@ export default function SettingsMacrosRoute() {
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
text="Edit"
|
||||
text={$at("Edit")}
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Edit macro ${macro.name}`}
|
||||
@@ -280,10 +282,11 @@ export default function SettingsMacrosRoute() {
|
||||
setShowDeleteConfirm(false);
|
||||
setMacroToDelete(null);
|
||||
}}
|
||||
title="Delete Macro"
|
||||
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
|
||||
title={$at("Delete Macro")}
|
||||
description={`${$at("Are you sure you want to delete")} "${macroToDelete?.name}" ${$at("? This action cannot be undone.")}`}
|
||||
variant="danger"
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? $at("Deleting...") : $at("Delete")}
|
||||
cancelText={$at("Cancel")}
|
||||
onConfirm={handleDeleteMacro}
|
||||
isConfirming={actionLoadingId === macroToDelete?.id}
|
||||
/>
|
||||
@@ -306,18 +309,18 @@ export default function SettingsMacrosRoute() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<SettingsPageHeader
|
||||
title="Keyboard Macros"
|
||||
description={`Combine keystrokes into a single action for faster workflows.`}
|
||||
title={$at("Keyboard Macros")}
|
||||
description={$at("Combine keystrokes into a single action for faster workflows.")}
|
||||
/>
|
||||
{macros.length > 0 && (
|
||||
<div className="flex items-center pl-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"}
|
||||
text={isMaxMacrosReached ? $at("Max Reached") : $at("Add New Macro")}
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
aria-label={$at("Add new macro")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -327,7 +330,7 @@ export default function SettingsMacrosRoute() {
|
||||
{loading && macros.length === 0 ? (
|
||||
<EmptyCard
|
||||
IconElm={LuCommand}
|
||||
headline="Loading macros..."
|
||||
headline={$at("Loading macros...")}
|
||||
BtnElm={
|
||||
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
@@ -337,16 +340,16 @@ export default function SettingsMacrosRoute() {
|
||||
) : macros.length === 0 ? (
|
||||
<EmptyCard
|
||||
IconElm={LuCommand}
|
||||
headline="Create Your First Macro"
|
||||
description="Combine keystrokes into a single action"
|
||||
headline={$at("Create Your First Macro")}
|
||||
description={$at("Combine keystrokes into a single action")}
|
||||
BtnElm={
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Add New Macro"
|
||||
text={$at("Add New Macro")}
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
aria-label={$at("Add new macro")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,10 @@ import { cx } from "../cva.config";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
export default function SettingsMouseRoute() {
|
||||
const { $at } = useReactAt();
|
||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
||||
|
||||
@@ -64,14 +67,14 @@ export default function SettingsMouseRoute() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Mouse"
|
||||
description="Configure cursor behavior and interaction settings for your device"
|
||||
title={$at("Mouse")}
|
||||
description={$at("Configure cursor behavior and interaction settings for your device")}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Hide Cursor"
|
||||
description="Hide the cursor when sending mouse movements"
|
||||
title={$at("Hide Cursor")}
|
||||
description={$at("Hide the cursor when sending mouse movements")}
|
||||
>
|
||||
<Checkbox
|
||||
checked={hideCursor}
|
||||
@@ -80,8 +83,8 @@ export default function SettingsMouseRoute() {
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Scroll Throttling"
|
||||
description="Reduce the frequency of scroll events"
|
||||
title={$at("Scroll Throttling")}
|
||||
description={$at("Reduce the frequency of scroll events")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -95,8 +98,8 @@ export default function SettingsMouseRoute() {
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Jiggler"
|
||||
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
||||
title={$at("Jiggler")}
|
||||
description={$at("Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating")}
|
||||
>
|
||||
<Checkbox
|
||||
checked={jiggler}
|
||||
@@ -104,7 +107,7 @@ export default function SettingsMouseRoute() {
|
||||
/>
|
||||
</SettingsItem>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="Modes" description="Choose the mouse input mode" />
|
||||
<SettingsItem title={$at("Modes")} description={$at("Choose the mouse input mode")} />
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="group block grow"
|
||||
@@ -122,10 +125,10 @@ export default function SettingsMouseRoute() {
|
||||
<div className="flex grow items-center justify-between">
|
||||
<div className="text-left">
|
||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||
Absolute
|
||||
{ $at("Absolute") }
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-800 dark:text-slate-300">
|
||||
Most convenient
|
||||
{ $at("Most convenient") }
|
||||
</p>
|
||||
</div>
|
||||
<CheckCircleIcon
|
||||
@@ -154,10 +157,10 @@ export default function SettingsMouseRoute() {
|
||||
<div className="flex grow items-center justify-between">
|
||||
<div className="text-left">
|
||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||
Relative
|
||||
{ $at("Relative") }
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-800 dark:text-slate-300">
|
||||
Most Compatible
|
||||
{ $at("Most Compatible") }
|
||||
</p>
|
||||
</div>
|
||||
<CheckCircleIcon
|
||||
|
||||
@@ -29,6 +29,7 @@ import AutoHeight from "../components/AutoHeight";
|
||||
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -71,6 +72,7 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||
}
|
||||
|
||||
export default function SettingsNetworkRoute() {
|
||||
const { $at } = useReactAt();
|
||||
const [send] = useJsonRpc();
|
||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
||||
state,
|
||||
@@ -215,13 +217,13 @@ export default function SettingsNetworkRoute() {
|
||||
<>
|
||||
<Fieldset disabled={!networkSettingsLoaded} className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Network"
|
||||
description="Configure your network settings"
|
||||
title={$at("Network")}
|
||||
description={$at("Configure your network settings")}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="MAC Address"
|
||||
description="Hardware identifier for the network interface"
|
||||
title={$at("MAC Address")}
|
||||
description={$at("Hardware identifier for the network interface")}
|
||||
>
|
||||
<InputField
|
||||
type="text"
|
||||
@@ -236,7 +238,7 @@ export default function SettingsNetworkRoute() {
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Hostname"
|
||||
description="Device identifier on the network. Blank for system default"
|
||||
description={$at("Device identifier on the network. Blank for system default")}
|
||||
>
|
||||
<div className="relative">
|
||||
<div>
|
||||
@@ -258,8 +260,8 @@ export default function SettingsNetworkRoute() {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<SettingsItem
|
||||
title="Domain"
|
||||
description="Network domain suffix for the device"
|
||||
title={$at("Domain")}
|
||||
description={$at("Device domain suffix in mDNS network")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<SelectMenuBasic
|
||||
@@ -279,7 +281,7 @@ export default function SettingsNetworkRoute() {
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
type="text"
|
||||
label="Custom Domain"
|
||||
label={$at("Custom Domain")}
|
||||
placeholder="home"
|
||||
value={customDomain}
|
||||
onChange={e => {
|
||||
@@ -293,7 +295,7 @@ export default function SettingsNetworkRoute() {
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="mDNS"
|
||||
description="Control mDNS (multicast DNS) operational mode"
|
||||
description={$at("Control mDNS (multicast DNS) operational mode")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -311,8 +313,8 @@ export default function SettingsNetworkRoute() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Time synchronization"
|
||||
description="Configure time synchronization settings"
|
||||
title={$at("Time synchronization")}
|
||||
description={$at("Configure time synchronization settings")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -336,7 +338,7 @@ export default function SettingsNetworkRoute() {
|
||||
size="SM"
|
||||
theme="primary"
|
||||
disabled={firstNetworkSettings.current === networkSettings}
|
||||
text="Save Settings"
|
||||
text={$at("Save settings")}
|
||||
onClick={() => setNetworkSettingsRemote(networkSettings)}
|
||||
/>
|
||||
</div>
|
||||
@@ -344,7 +346,7 @@ export default function SettingsNetworkRoute() {
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
||||
<SettingsItem title={$at("IPv4 Mode")} description={$at("Configure IPv4 mode")}>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv4_mode}
|
||||
@@ -361,7 +363,7 @@ export default function SettingsNetworkRoute() {
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
DHCP Lease Information
|
||||
{$at("DHCP Lease Information")}
|
||||
</h3>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||
@@ -379,14 +381,14 @@ export default function SettingsNetworkRoute() {
|
||||
) : (
|
||||
<EmptyCard
|
||||
IconElm={LuEthernetPort}
|
||||
headline="DHCP Information"
|
||||
description="No DHCP lease information available"
|
||||
headline={$at("DHCP Information")}
|
||||
description={$at("No DHCP lease information available")}
|
||||
/>
|
||||
)}
|
||||
</AutoHeight>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||
<SettingsItem title={$at("IPv6 Mode")} description={$at("Configure the IPv6 mode")}>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv6_mode}
|
||||
@@ -423,8 +425,8 @@ export default function SettingsNetworkRoute() {
|
||||
) : (
|
||||
<EmptyCard
|
||||
IconElm={LuEthernetPort}
|
||||
headline="IPv6 Information"
|
||||
description="No IPv6 addresses configured"
|
||||
headline={$at("IPv6 Information")}
|
||||
description={$at("No IPv6 addresses configured")}
|
||||
/>
|
||||
)}
|
||||
</AutoHeight>
|
||||
@@ -450,10 +452,11 @@ export default function SettingsNetworkRoute() {
|
||||
<ConfirmDialog
|
||||
open={showRenewLeaseConfirm}
|
||||
onClose={() => setShowRenewLeaseConfirm(false)}
|
||||
title="Renew DHCP Lease"
|
||||
description="This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process."
|
||||
title={$at("Renew DHCP Lease")}
|
||||
description={$at("This will request your DHCP server to assign a new IP address. Your device may lose network connectivity during the process.")}
|
||||
variant="danger"
|
||||
confirmText="Renew Lease"
|
||||
confirmText={$at("Renew DHCP Lease")}
|
||||
cancelText={$at("Cancel")}
|
||||
onConfirm={() => {
|
||||
handleRenewLease();
|
||||
setShowRenewLeaseConfirm(false);
|
||||
|
||||
@@ -24,6 +24,7 @@ import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { cx } from "../cva.config";
|
||||
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
export default function SettingsRoute() {
|
||||
@@ -34,6 +35,7 @@ export default function SettingsRoute() {
|
||||
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
||||
const [showRightGradient, setShowRightGradient] = useState(false);
|
||||
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
|
||||
const {setCurrentLang,$at,langSet}= useReactAt()
|
||||
|
||||
// Handle scroll position to show/hide gradients
|
||||
const handleScroll = () => {
|
||||
@@ -92,7 +94,7 @@ export default function SettingsRoute() {
|
||||
to=".."
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Back to KVM"
|
||||
text={$at("Back to KVM")}
|
||||
LeadingIcon={LuArrowLeft}
|
||||
textAlign="left"
|
||||
/>
|
||||
@@ -102,7 +104,7 @@ export default function SettingsRoute() {
|
||||
to=".."
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text="Back to KVM"
|
||||
text={$at("Back to KVM")}
|
||||
LeadingIcon={LuArrowLeft}
|
||||
textAlign="left"
|
||||
fullWidth
|
||||
@@ -141,7 +143,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuSettings className="h-4 w-4 shrink-0" />
|
||||
<h1>General</h1>
|
||||
<h1>{$at("General")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -153,11 +155,11 @@ export default function SettingsRoute() {
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
|
||||
<LuMouse className="h-4 w-4 shrink-0" />
|
||||
<h1>Mouse</h1>
|
||||
<h1>{$at("Mouse")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<FeatureFlag minAppVersion="0.4.0" name="Paste text">
|
||||
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
to="keyboard"
|
||||
@@ -165,11 +167,11 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<h1>Keyboard</h1>
|
||||
<h1>{$at("Keyboard")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
</FeatureFlag>
|
||||
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
to="video"
|
||||
@@ -177,7 +179,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuVideo className="h-4 w-4 shrink-0" />
|
||||
<h1>Video</h1>
|
||||
<h1>{$at("Video")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -188,7 +190,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuCpu className="h-4 w-4 shrink-0" />
|
||||
<h1>Hardware</h1>
|
||||
<h1>{$at("Hardware")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -199,7 +201,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuShieldCheck className="h-4 w-4 shrink-0" />
|
||||
<h1>Access</h1>
|
||||
<h1>{$at("Access")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -210,7 +212,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuPalette className="h-4 w-4 shrink-0" />
|
||||
<h1>Appearance</h1>
|
||||
<h1>{$at("Appearance")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -221,7 +223,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuCommand className="h-4 w-4 shrink-0" />
|
||||
<h1>Keyboard Macros</h1>
|
||||
<h1>{$at("Keyboard Macros")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -232,7 +234,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuNetwork className="h-4 w-4 shrink-0" />
|
||||
<h1>Network</h1>
|
||||
<h1>{$at("Network")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
@@ -243,7 +245,7 @@ export default function SettingsRoute() {
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuWrench className="h-4 w-4 shrink-0" />
|
||||
<h1>Advanced</h1>
|
||||
<h1>{$at("Advanced")}</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@ import notifications from "../notifications";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
|
||||
const defaultEdid =
|
||||
"00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b";
|
||||
const edids = [
|
||||
@@ -41,6 +43,7 @@ const streamQualityOptions = [
|
||||
];
|
||||
|
||||
export default function SettingsVideoRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const [send] = useJsonRpc();
|
||||
const [streamQuality, setStreamQuality] = useState("1");
|
||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||
@@ -117,15 +120,15 @@ export default function SettingsVideoRoute() {
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Video"
|
||||
description="Configure display settings and EDID for optimal compatibility"
|
||||
title={$at("Video")}
|
||||
description={$at("Configure display settings and EDID for optimal compatibility")}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Stream Quality"
|
||||
description="Adjust the quality of the video stream"
|
||||
title={$at("Stream Quality")}
|
||||
description={$at("Adjust the quality of the video stream")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -138,14 +141,14 @@ export default function SettingsVideoRoute() {
|
||||
|
||||
{/* Video Enhancement Settings */}
|
||||
<SettingsItem
|
||||
title="Video Enhancement"
|
||||
description="Adjust color settings to make the video output more vibrant and colorful"
|
||||
title={$at("Video Enhancement")}
|
||||
description={$at("Adjust color settings to make the video output more vibrant and colorful")}
|
||||
/>
|
||||
|
||||
<div className="space-y-4 pl-4">
|
||||
<SettingsItem
|
||||
title="Saturation"
|
||||
description={`Color saturation (${videoSaturation.toFixed(1)}x)`}
|
||||
title={$at("Saturation")}
|
||||
description={`${$at("Color saturation")} (${videoSaturation.toFixed(1)}x)`}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
@@ -159,8 +162,8 @@ export default function SettingsVideoRoute() {
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Brightness"
|
||||
description={`Brightness level (${videoBrightness.toFixed(1)}x)`}
|
||||
title={$at("Brightness")}
|
||||
description={`${$at("Brightness level")} (${videoBrightness.toFixed(1)}x)`}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
@@ -174,8 +177,8 @@ export default function SettingsVideoRoute() {
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Contrast"
|
||||
description={`Contrast level (${videoContrast.toFixed(1)}x)`}
|
||||
title={$at("Contrast")}
|
||||
description={`${$at("Contrast level")} (${videoContrast.toFixed(1)}x)`}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
@@ -192,7 +195,7 @@ export default function SettingsVideoRoute() {
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reset to Default"
|
||||
text={$at("Reset to Default")}
|
||||
onClick={() => {
|
||||
setVideoSaturation(1.0);
|
||||
setVideoBrightness(1.0);
|
||||
@@ -204,7 +207,7 @@ export default function SettingsVideoRoute() {
|
||||
|
||||
<SettingsItem
|
||||
title="EDID"
|
||||
description="Adjust the EDID settings for the display"
|
||||
description={$at("Adjust the EDID settings for the display")}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
@@ -226,11 +229,11 @@ export default function SettingsVideoRoute() {
|
||||
{customEdidValue !== null && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="Custom EDID"
|
||||
description="EDID details video mode compatibility. Default settings works in most cases, but unique UEFI/BIOS might need adjustments."
|
||||
title={$at("Custom EDID")}
|
||||
description={$at("EDID details video mode compatibility. Default settings works in most cases, but unique UEFI/BIOS might need adjustments.")}
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label="EDID File"
|
||||
label={$at("EDID File")}
|
||||
placeholder="00F..."
|
||||
rows={3}
|
||||
value={customEdidValue}
|
||||
@@ -240,13 +243,13 @@ export default function SettingsVideoRoute() {
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Set Custom EDID"
|
||||
text={$at("Set Custom EDID")}
|
||||
onClick={() => handleEDIDChange(customEdidValue)}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Restore to default"
|
||||
text={$at("Restore to default")}
|
||||
onClick={() => {
|
||||
setCustomEdidValue(null);
|
||||
handleEDIDChange(defaultEdid);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import GridBackground from "@components/GridBackground";
|
||||
import Container from "@components/Container";
|
||||
@@ -10,6 +10,9 @@ import { DEVICE_API } from "@/ui.config";
|
||||
import { GridCard } from "../components/Card";
|
||||
import { cx } from "../cva.config";
|
||||
import api from "../api";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import {useReactAt} from 'i18n-auto-extractor/react'
|
||||
import DashboardNavbar from "@/components/Header";
|
||||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
@@ -47,6 +50,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||
};
|
||||
|
||||
export default function WelcomeLocalModeRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const actionData = useActionData() as { error?: string };
|
||||
const [selectedMode, setSelectedMode] = useState<"password" | "noPassword" | null>(
|
||||
null,
|
||||
@@ -54,6 +58,12 @@ export default function WelcomeLocalModeRoute() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardNavbar
|
||||
primaryLinks={[]}
|
||||
showConnectionStatus={false}
|
||||
isLoggedIn={false}
|
||||
kvmName={"PicoKVM Device"}
|
||||
/>
|
||||
<GridBackground />
|
||||
<div className="grid min-h-screen">
|
||||
<Container>
|
||||
@@ -73,10 +83,10 @@ export default function WelcomeLocalModeRoute() {
|
||||
style={{ animationDelay: "200ms" }}
|
||||
>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Local Authentication Method
|
||||
{($at("Local Authentication Method"))}
|
||||
</h1>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400">
|
||||
Select how you{"'"}d like to secure your KVM device locally.
|
||||
{($at("Select how you would like to secure your KVM device locally."))}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -101,11 +111,19 @@ export default function WelcomeLocalModeRoute() {
|
||||
<h3 className="text-base font-bold text-black dark:text-white">
|
||||
{mode === "password" ? "Password protected" : "No Password"}
|
||||
</h3>
|
||||
<h3 className="text-base font-bold text-black dark:text-white">
|
||||
{mode === "password" ? "密码保护" : "无密码"}
|
||||
</h3>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
{mode === "password"
|
||||
? "Secure your device with a password for added protection."
|
||||
: "Quick access without password authentication."}
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
{mode === "password"
|
||||
? "设置密码保护您的设备安全"
|
||||
: "无需密码快速访问"}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -140,7 +158,7 @@ export default function WelcomeLocalModeRoute() {
|
||||
theme="primary"
|
||||
fullWidth
|
||||
type="submit"
|
||||
text="Continue"
|
||||
text={$at("Continue")}
|
||||
textAlign="center"
|
||||
disabled={!selectedMode}
|
||||
/>
|
||||
@@ -151,7 +169,7 @@ export default function WelcomeLocalModeRoute() {
|
||||
className="animate-fadeIn mx-auto max-w-md text-center text-xs text-slate-500 opacity-0 dark:text-slate-400"
|
||||
style={{ animationDelay: "600ms" }}
|
||||
>
|
||||
You can always change your authentication method later in the settings.
|
||||
{($at("You can always change your authentication method later in the settings."))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import LogoLuckfox from "@/assets/logo-luckfox.png";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import { useReactAt } from 'i18n-auto-extractor/react'
|
||||
import DashboardNavbar from "@/components/Header";
|
||||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
@@ -50,6 +52,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||
};
|
||||
|
||||
export default function WelcomeLocalPasswordRoute() {
|
||||
const { $at }= useReactAt();
|
||||
const actionData = useActionData() as { error?: string };
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -65,11 +68,17 @@ export default function WelcomeLocalPasswordRoute() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardNavbar
|
||||
primaryLinks={[]}
|
||||
showConnectionStatus={false}
|
||||
isLoggedIn={false}
|
||||
kvmName={"PicoKVM Device"}
|
||||
/>
|
||||
<GridBackground />
|
||||
<div className="grid min-h-screen">
|
||||
<Container>
|
||||
<div className="isolate flex h-full w-full items-center justify-center">
|
||||
<div className="max-w-2xl space-y-8">
|
||||
<div className="max-w-2xl space-y-8">
|
||||
<div className="animate-fadeIn flex items-center justify-center opacity-0">
|
||||
<img
|
||||
src={LogoLuckfox}
|
||||
@@ -84,10 +93,10 @@ export default function WelcomeLocalPasswordRoute() {
|
||||
style={{ animationDelay: "200ms" }}
|
||||
>
|
||||
<h1 className="text-4xl font-semibold text-black dark:text-white">
|
||||
Set a Password
|
||||
{$at("Set a Password")}
|
||||
</h1>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400">
|
||||
Create a strong password to secure your KVM device locally.
|
||||
{$at("Create a strong password to secure your KVM device locally.")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -99,10 +108,10 @@ export default function WelcomeLocalPasswordRoute() {
|
||||
style={{ animationDelay: "400ms" }}
|
||||
>
|
||||
<InputFieldWithLabel
|
||||
label="Password"
|
||||
label={$at("Password")}
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
placeholder="Enter a password"
|
||||
placeholder={$at("Enter a password")}
|
||||
autoComplete="new-password"
|
||||
ref={passwordInputRef}
|
||||
TrailingElm={
|
||||
@@ -129,11 +138,11 @@ export default function WelcomeLocalPasswordRoute() {
|
||||
style={{ animationDelay: "400ms" }}
|
||||
>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm Password"
|
||||
label={$at("Confirm Password")}
|
||||
autoComplete="new-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm your password"
|
||||
placeholder={$at("Confirm your password")}
|
||||
error={actionData?.error}
|
||||
/>
|
||||
</div>
|
||||
@@ -150,7 +159,7 @@ export default function WelcomeLocalPasswordRoute() {
|
||||
theme="primary"
|
||||
fullWidth
|
||||
type="submit"
|
||||
text="Set Password"
|
||||
text={$at("Set Password")}
|
||||
textAlign="center"
|
||||
/>
|
||||
</div>
|
||||
@@ -161,15 +170,15 @@ export default function WelcomeLocalPasswordRoute() {
|
||||
className="animate-fadeIn max-w-md text-center text-xs text-slate-500 opacity-0 dark:text-slate-400"
|
||||
style={{ animationDelay: "800ms" }}
|
||||
>
|
||||
This password will be used to secure your device data and protect against
|
||||
unauthorized access.{" "}
|
||||
<span className="font-bold">All data remains on your local device.</span>
|
||||
{$at("This password will be used to secure your device data and protect against unauthorized access.")}{" "}
|
||||
<span className="font-bold">{$at("All data remains on your local device.")}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user