Add language validation
This commit is contained in:
parent
f3af1d08d8
commit
c7bf9c14f8
@ -10,6 +10,9 @@ const nextConfig = {
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
},
|
||||
compiler: {
|
||||
removeConsole: process.env.NODE_ENV === "production",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
@ -39,7 +39,7 @@
|
||||
"clean": "rimraf dist renderer/.next renderer/out",
|
||||
"start": "tsc && electron .",
|
||||
"dev": "tsc && electron .",
|
||||
"build": "tsc && next build renderer",
|
||||
"build": "tsc && npm run validate-schema && next build renderer",
|
||||
"tsc": "tsc",
|
||||
"pack-app": "tsc && npm run build && electron-builder --dir",
|
||||
"dist": "tsc && npm run build && cross-env DEBUG=* electron-builder",
|
||||
@ -66,7 +66,8 @@
|
||||
"publish-mac-arm-app": "tsc && npm run build && electron-builder --mac --arm64 --publish always",
|
||||
"dist:appstore": "CSC_KEY_PASSWORD=$PASSWORD CSC_LINK=$(openssl base64 -in $CERTIFICATE_PATH) npm run dist:mac-mas",
|
||||
"enable-store": "sed -i '' -e 's/APP_STORE_BUILD: false,/APP_STORE_BUILD: true,/' common/feature-flags.ts",
|
||||
"disable-store": "sed -i '' -e 's/APP_STORE_BUILD: true,/APP_STORE_BUILD: false,/' common/feature-flags.ts"
|
||||
"disable-store": "sed -i '' -e 's/APP_STORE_BUILD: true,/APP_STORE_BUILD: false,/' common/feature-flags.ts",
|
||||
"validate-schema": "node scripts/validate-schema.js"
|
||||
},
|
||||
"build": {
|
||||
"productName": "Upscayl",
|
||||
|
145
renderer/components/main-content/onboarding-dialog.tsx
Normal file
145
renderer/components/main-content/onboarding-dialog.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
|
||||
type OnboardingStep = {
|
||||
title: string;
|
||||
description: string;
|
||||
type: "info" | "settings";
|
||||
settings?: {
|
||||
type: "switch" | "input";
|
||||
label: string;
|
||||
key: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const onboardingSteps: OnboardingStep[] = [
|
||||
{
|
||||
title: "Welcome to Upscayl 🎉",
|
||||
description: "Let's get you started with a few quick steps.",
|
||||
type: "info",
|
||||
},
|
||||
{
|
||||
title: "Choose Your Preferences",
|
||||
description: "Configure your initial settings.",
|
||||
type: "settings",
|
||||
settings: [
|
||||
{
|
||||
type: "switch",
|
||||
label: "Enable automatic updates",
|
||||
key: "autoUpdate",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
label: "Default output folder",
|
||||
key: "outputFolder",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "You're All Set!",
|
||||
description: "You can now start upscaling your images.",
|
||||
type: "info",
|
||||
},
|
||||
];
|
||||
|
||||
export function OnboardingDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [settings, setSettings] = useState<Record<string, any>>({});
|
||||
|
||||
const currentStepData = onboardingSteps[currentStep];
|
||||
const isLastStep = currentStep === onboardingSteps.length - 1;
|
||||
const isFirstStep = currentStep === 0;
|
||||
|
||||
const handleNext = () => {
|
||||
if (isLastStep) {
|
||||
onOpenChange(false);
|
||||
// Here you can handle saving the settings
|
||||
console.log("Final settings:", settings);
|
||||
} else {
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSettingChange = (key: string, value: any) => {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex h-[80%] w-full max-w-[80%] flex-col items-center justify-center">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center text-4xl">
|
||||
{currentStepData.title}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-lg">
|
||||
{currentStepData.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{currentStepData.type === "settings" && currentStepData.settings && (
|
||||
<div className="grid gap-4 py-4">
|
||||
{currentStepData.settings.map((setting) => (
|
||||
<div
|
||||
key={setting.key}
|
||||
className="grid grid-cols-2 items-center gap-4"
|
||||
>
|
||||
<label htmlFor={setting.key} className="text-right">
|
||||
{setting.label}
|
||||
</label>
|
||||
{setting.type === "switch" && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle"
|
||||
defaultChecked
|
||||
id={setting.key}
|
||||
checked={settings[setting.key] || false}
|
||||
onChange={(e) =>
|
||||
handleSettingChange(setting.key, e.target.checked)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{setting.type === "input" && (
|
||||
<input
|
||||
className="input input-bordered"
|
||||
id={setting.key}
|
||||
value={settings[setting.key] || ""}
|
||||
onChange={(e) =>
|
||||
handleSettingChange(setting.key, e.target.value)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<button onClick={handleNext} className="btn btn-secondary">
|
||||
{isLastStep ? "Get Started" : "Next"}
|
||||
</button>
|
||||
{!isFirstStep && (
|
||||
<button
|
||||
onClick={() => setCurrentStep((prev) => prev - 1)}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@ -17,18 +17,15 @@ import { selectedModelIdAtom } from "@/atoms/user-settings-atom";
|
||||
import { customModelIdsAtom } from "@/atoms/models-list-atom";
|
||||
import useTranslation from "@/components/hooks/use-translation";
|
||||
|
||||
export default function SelectModel() {
|
||||
const SelectModelDialog = () => {
|
||||
const t = useTranslation();
|
||||
const [selectedModelId, setSelectedModelId] = useAtom(selectedModelIdAtom);
|
||||
console.log("🚀 => selectedModelId:", selectedModelId);
|
||||
|
||||
const customModelIds = useAtomValue(customModelIdsAtom);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [zoomedModel, setZoomedModel] = useState<ModelId | null>(null);
|
||||
|
||||
const handleModelSelect = (model: ModelId | string) => {
|
||||
console.log("🚀 => model:", model);
|
||||
|
||||
setSelectedModelId(model);
|
||||
setOpen(false);
|
||||
};
|
||||
@ -160,4 +157,6 @@ export default function SelectModel() {
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SelectModelDialog;
|
@ -16,7 +16,7 @@ import { ELECTRON_COMMANDS } from "@common/electron-commands";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { translationAtom } from "@/atoms/translations-atom";
|
||||
import { SelectImageScale } from "../settings-tab/select-image-scale";
|
||||
import SelectModel from "./select-model";
|
||||
import SelectModelDialog from "./select-model-dialog";
|
||||
import { ImageFormat } from "@/lib/valid-formats";
|
||||
|
||||
interface IProps {
|
||||
@ -156,7 +156,7 @@ function UpscaylSteps({
|
||||
<p className="step-heading">{t("APP.MODEL_SELECTION.TITLE")}</p>
|
||||
<p className="mb-2 text-sm">{t("APP.MODEL_SELECTION.DESCRIPTION")}</p>
|
||||
|
||||
<SelectModel />
|
||||
<SelectModelDialog />
|
||||
</div>
|
||||
|
||||
{!batchMode && (
|
||||
|
@ -20,6 +20,7 @@ import getDirectoryFromPath from "@common/get-directory-from-path";
|
||||
import { FEATURE_FLAGS } from "@common/feature-flags";
|
||||
import { ImageFormat, VALID_IMAGE_FORMATS } from "@/lib/valid-formats";
|
||||
import { initCustomModels } from "@/components/hooks/use-custom-models";
|
||||
import { OnboardingDialog } from "@/components/main-content/onboarding-dialog";
|
||||
|
||||
const Home = () => {
|
||||
const t = useAtomValue(translationAtom);
|
||||
@ -310,6 +311,7 @@ const Home = () => {
|
||||
doubleUpscaylCounter={doubleUpscaylCounter}
|
||||
setDimensions={setDimensions}
|
||||
/>
|
||||
<OnboardingDialog open={false} onOpenChange={() => {}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
52
scripts/generate-schema.js
Normal file
52
scripts/generate-schema.js
Normal file
@ -0,0 +1,52 @@
|
||||
function generateSchema(json) {
|
||||
if (json === null) {
|
||||
return { type: "null" };
|
||||
}
|
||||
|
||||
const type = Array.isArray(json) ? "array" : typeof json;
|
||||
const schema = { type };
|
||||
|
||||
switch (type) {
|
||||
case "object":
|
||||
const properties = {};
|
||||
const required = [];
|
||||
|
||||
for (const [key, value] of Object.entries(json)) {
|
||||
properties[key] = generateSchema(value);
|
||||
required.push(key);
|
||||
}
|
||||
|
||||
schema.properties = properties;
|
||||
if (required.length > 0) {
|
||||
schema.required = required;
|
||||
}
|
||||
break;
|
||||
|
||||
case "array":
|
||||
if (json.length > 0) {
|
||||
// Assume all items in array are of the same type as the first item
|
||||
schema.items = generateSchema(json[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
module.exports = { generateSchema };
|
||||
|
||||
// Example usage:
|
||||
/*
|
||||
const obj = {
|
||||
name: "John",
|
||||
age: 30,
|
||||
address: {
|
||||
street: "123 Main St",
|
||||
city: "Boston"
|
||||
},
|
||||
hobbies: ["reading", "swimming"]
|
||||
};
|
||||
|
||||
const schema = generateSchema(obj);
|
||||
console.log(JSON.stringify(schema, null, 2));
|
||||
*/
|
36
scripts/validate-schema.js
Normal file
36
scripts/validate-schema.js
Normal file
@ -0,0 +1,36 @@
|
||||
const Ajv = require("ajv");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { generateSchema } = require("./generate-schema");
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
console.log("Validating language files...");
|
||||
|
||||
// Load the base language file (en.json)
|
||||
const enJsonPath = path.join(__dirname, "..", "renderer", "locales", "en.json");
|
||||
const enJson = JSON.parse(fs.readFileSync(enJsonPath, "utf-8"));
|
||||
|
||||
// Generate the schema for en.json
|
||||
const enSchema = generateSchema(enJson);
|
||||
const validate = ajv.compile(enSchema);
|
||||
|
||||
// Validate other language files
|
||||
const localesDir = path.join(__dirname, "..", "renderer", "locales");
|
||||
const languageFiles = fs
|
||||
.readdirSync(localesDir)
|
||||
.filter((file) => file !== "en.json");
|
||||
|
||||
languageFiles.forEach((file) => {
|
||||
const filePath = path.join(localesDir, file);
|
||||
const jsonData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
|
||||
const valid = validate(jsonData);
|
||||
|
||||
if (!valid) {
|
||||
console.error(`Errors in ${file}:`);
|
||||
console.error(validate.errors);
|
||||
} else {
|
||||
console.log(`${file} is valid.`);
|
||||
}
|
||||
});
|
@ -15,6 +15,12 @@
|
||||
"@common/*": ["./common/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./electron/**/*", "./common/**/*", "./renderer/**/*"],
|
||||
"include": [
|
||||
"./electron/**/*",
|
||||
"./common/**/*",
|
||||
"./renderer/**/*",
|
||||
"scripts/generate-schema.js",
|
||||
"scripts/validate-schema.js"
|
||||
],
|
||||
"exclude": ["node_modules", "public", "renderer"]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user