Update App version to 0.0.3

This commit is contained in:
luckfox-eng29
2025-09-25 16:51:53 +08:00
parent 15d276652c
commit 4e82b8a11c
59 changed files with 2841 additions and 794 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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={() => {

View File

@@ -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>
);

View File

@@ -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();

View File

@@ -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);

View File

@@ -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>
);
);
}

View File

@@ -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;

View File

@@ -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}

View File

@@ -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={{

View File

@@ -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);

View File

@@ -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")}
/>
}
/>

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>
</>
);
}