Add language validation
This commit is contained in:
parent
f3af1d08d8
commit
c7bf9c14f8
@ -10,6 +10,9 @@ const nextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
externalDir: true,
|
externalDir: true,
|
||||||
},
|
},
|
||||||
|
compiler: {
|
||||||
|
removeConsole: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
"clean": "rimraf dist renderer/.next renderer/out",
|
"clean": "rimraf dist renderer/.next renderer/out",
|
||||||
"start": "tsc && electron .",
|
"start": "tsc && electron .",
|
||||||
"dev": "tsc && electron .",
|
"dev": "tsc && electron .",
|
||||||
"build": "tsc && next build renderer",
|
"build": "tsc && npm run validate-schema && next build renderer",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"pack-app": "tsc && npm run build && electron-builder --dir",
|
"pack-app": "tsc && npm run build && electron-builder --dir",
|
||||||
"dist": "tsc && npm run build && cross-env DEBUG=* electron-builder",
|
"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",
|
"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",
|
"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",
|
"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": {
|
"build": {
|
||||||
"productName": "Upscayl",
|
"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 { customModelIdsAtom } from "@/atoms/models-list-atom";
|
||||||
import useTranslation from "@/components/hooks/use-translation";
|
import useTranslation from "@/components/hooks/use-translation";
|
||||||
|
|
||||||
export default function SelectModel() {
|
const SelectModelDialog = () => {
|
||||||
const t = useTranslation();
|
const t = useTranslation();
|
||||||
const [selectedModelId, setSelectedModelId] = useAtom(selectedModelIdAtom);
|
const [selectedModelId, setSelectedModelId] = useAtom(selectedModelIdAtom);
|
||||||
console.log("🚀 => selectedModelId:", selectedModelId);
|
|
||||||
|
|
||||||
const customModelIds = useAtomValue(customModelIdsAtom);
|
const customModelIds = useAtomValue(customModelIdsAtom);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [zoomedModel, setZoomedModel] = useState<ModelId | null>(null);
|
const [zoomedModel, setZoomedModel] = useState<ModelId | null>(null);
|
||||||
|
|
||||||
const handleModelSelect = (model: ModelId | string) => {
|
const handleModelSelect = (model: ModelId | string) => {
|
||||||
console.log("🚀 => model:", model);
|
|
||||||
|
|
||||||
setSelectedModelId(model);
|
setSelectedModelId(model);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
@ -160,4 +157,6 @@ export default function SelectModel() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default SelectModelDialog;
|
@ -16,7 +16,7 @@ import { ELECTRON_COMMANDS } from "@common/electron-commands";
|
|||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { translationAtom } from "@/atoms/translations-atom";
|
import { translationAtom } from "@/atoms/translations-atom";
|
||||||
import { SelectImageScale } from "../settings-tab/select-image-scale";
|
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";
|
import { ImageFormat } from "@/lib/valid-formats";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@ -156,7 +156,7 @@ function UpscaylSteps({
|
|||||||
<p className="step-heading">{t("APP.MODEL_SELECTION.TITLE")}</p>
|
<p className="step-heading">{t("APP.MODEL_SELECTION.TITLE")}</p>
|
||||||
<p className="mb-2 text-sm">{t("APP.MODEL_SELECTION.DESCRIPTION")}</p>
|
<p className="mb-2 text-sm">{t("APP.MODEL_SELECTION.DESCRIPTION")}</p>
|
||||||
|
|
||||||
<SelectModel />
|
<SelectModelDialog />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!batchMode && (
|
{!batchMode && (
|
||||||
|
@ -20,6 +20,7 @@ import getDirectoryFromPath from "@common/get-directory-from-path";
|
|||||||
import { FEATURE_FLAGS } from "@common/feature-flags";
|
import { FEATURE_FLAGS } from "@common/feature-flags";
|
||||||
import { ImageFormat, VALID_IMAGE_FORMATS } from "@/lib/valid-formats";
|
import { ImageFormat, VALID_IMAGE_FORMATS } from "@/lib/valid-formats";
|
||||||
import { initCustomModels } from "@/components/hooks/use-custom-models";
|
import { initCustomModels } from "@/components/hooks/use-custom-models";
|
||||||
|
import { OnboardingDialog } from "@/components/main-content/onboarding-dialog";
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const t = useAtomValue(translationAtom);
|
const t = useAtomValue(translationAtom);
|
||||||
@ -310,6 +311,7 @@ const Home = () => {
|
|||||||
doubleUpscaylCounter={doubleUpscaylCounter}
|
doubleUpscaylCounter={doubleUpscaylCounter}
|
||||||
setDimensions={setDimensions}
|
setDimensions={setDimensions}
|
||||||
/>
|
/>
|
||||||
|
<OnboardingDialog open={false} onOpenChange={() => {}} />
|
||||||
</div>
|
</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/*"]
|
"@common/*": ["./common/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["./electron/**/*", "./common/**/*", "./renderer/**/*"],
|
"include": [
|
||||||
|
"./electron/**/*",
|
||||||
|
"./common/**/*",
|
||||||
|
"./renderer/**/*",
|
||||||
|
"scripts/generate-schema.js",
|
||||||
|
"scripts/validate-schema.js"
|
||||||
|
],
|
||||||
"exclude": ["node_modules", "public", "renderer"]
|
"exclude": ["node_modules", "public", "renderer"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user